From ff186b58e8cf14cdacc2a3f768b5748027dc029a Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Wed, 10 Jul 2024 01:02:40 +0300 Subject: [PATCH 01/16] Cleanup obsolete Cloud sync code (#3317) * Deprecate `isSynced` and `isDeleted` * Remove sync fields from the domain model * Get rid of the `TransactionRepository` abstraction * Remove unnecessary `TransactionRepository` abstraction * Remove `flagDeleted*` * WIP: Migrate `flagDeleted` to direct deletion * Make the project build * WIP: Remove unnecessary abstractions * Fix build errors * Add delete flagged data migration * Fix Detekt errors * Fix broken tests * Ensure no deleted transactions are shown * Ensure no deleted accounts are shown * Fix unit tests * Fix Detekt errors * Fix broken tests * WIP: Add migration tests * Add test for deleting deleted transactions * Add a test case for migrating deleted accounts * Add a test case for migrating deleted categories * Add a test case for migrating deleted loans --- .gitignore | 5 + .../main/java/com/ivy/accounts/AccountsTab.kt | 22 +- .../main/java/com/ivy/budgets/BudgetModal.kt | 13 - .../com/ivy/categories/CategoriesScreen.kt | 11 - .../importdata/csv/domain/CSVImporterV2.kt | 3 - .../onboarding/steps/OnboardingCategories.kt | 5 - .../main/java/com/ivy/piechart/PieChart.kt | 25 +- .../ivy/piechart/PieChartStatisticScreen.kt | 27 +- .../com/ivy/piechart/action/PieChartAct.kt | 3 - .../ivy/planned/edit/EditPlannedViewModel.kt | 2 +- .../ivy/planned/list/PlannedPaymentCard.kt | 7 - .../ivy/planned/list/PlannedPaymentsScreen.kt | 7 +- .../java/com/ivy/reports/FilterOverlay.kt | 11 +- .../main/java/com/ivy/reports/ReportScreen.kt | 13 - .../java/com/ivy/reports/ReportViewModel.kt | 3 - .../ivy/transactions/TransactionsViewModel.kt | 9 +- .../com.ivy.data.db.IvyRoomDatabase/129.json | 838 ++++++++++++++++++ .../backup/BackupDataUseCaseAndroidTest.kt | 26 +- .../data/db/IvyRoomDatabaseMigrationTest.kt | 8 +- .../com/ivy/data/db/Migration128to129Test.kt | 297 +++++++ .../java/com/ivy/data/db/IvyRoomDatabase.kt | 6 +- .../ivy/data/db/dao/fake/FakeAccountDao.kt | 5 - .../com/ivy/data/db/dao/fake/FakeBudgetDao.kt | 4 - .../ivy/data/db/dao/fake/FakeCategoryDao.kt | 10 - .../com/ivy/data/db/dao/fake/FakeLoanDao.kt | 4 - .../ivy/data/db/dao/fake/FakeLoanRecordDao.kt | 4 - .../data/db/dao/fake/FakePlannedPaymentDao.kt | 8 +- .../data/db/dao/fake/FakeTransactionDao.kt | 30 +- .../ivy/data/db/dao/write/WriteAccountDao.kt | 3 - .../ivy/data/db/dao/write/WriteBudgetDao.kt | 3 - .../ivy/data/db/dao/write/WriteCategoryDao.kt | 3 - .../com/ivy/data/db/dao/write/WriteLoanDao.kt | 3 - .../data/db/dao/write/WriteLoanRecordDao.kt | 3 - .../dao/write/WritePlannedPaymentRuleDao.kt | 5 +- .../data/db/dao/write/WriteTransactionDao.kt | 13 +- .../com/ivy/data/db/entity/AccountEntity.kt | 3 + .../com/ivy/data/db/entity/BudgetEntity.kt | 3 + .../com/ivy/data/db/entity/CategoryEntity.kt | 3 + .../java/com/ivy/data/db/entity/LoanEntity.kt | 4 + .../ivy/data/db/entity/LoanRecordEntity.kt | 2 + .../db/entity/PlannedPaymentRuleEntity.kt | 3 + .../com/ivy/data/db/entity/SettingsEntity.kt | 3 + .../data/db/entity/TagAssociationEntity.kt | 2 + .../java/com/ivy/data/db/entity/TagEntity.kt | 8 +- .../ivy/data/db/entity/TransactionEntity.kt | 2 + .../Migration128to129_DeleteIsDeleted.kt | 26 + .../ivy/data/di/RepositoryBindingsModule.kt | 45 - .../ivy/data/repository/AccountRepository.kt | 73 +- .../ivy/data/repository/CategoryRepository.kt | 75 +- .../ivy/data/repository/CurrencyRepository.kt | 60 +- .../repository/ExchangeRatesRepository.kt | 75 +- .../ivy/data/repository/LegalRepository.kt | 21 +- .../com/ivy/data/repository/RepositoryMemo.kt | 6 +- .../com/ivy/data/repository/TagRepository.kt | 180 +++- .../data/repository/TransactionRepository.kt | 319 ++++++- .../repository/fake/FakeAccountRepository.kt | 31 - .../repository/fake/FakeCurrencyRepository.kt | 19 - .../repository/fake/FakeRepositoryMemo.kt | 2 +- .../data/repository/fake/FakeTagRepository.kt | 28 - .../repository/impl/AccountRepositoryImpl.kt | 75 -- .../repository/impl/CategoryRepositoryImpl.kt | 77 -- .../repository/impl/CurrencyRepositoryImpl.kt | 63 -- .../impl/ExchangeRatesRepositoryImpl.kt | 79 -- .../repository/impl/LegalRepositoryImpl.kt | 22 - .../data/repository/impl/TagRepositoryImpl.kt | 175 ---- .../impl/TransactionRepositoryImpl.kt | 367 -------- .../data/repository/mapper/AccountMapper.kt | 7 +- .../data/repository/mapper/CategoryMapper.kt | 14 +- .../ivy/data/repository/mapper/TagMapper.kt | 16 +- .../repository/mapper/TransactionMapper.kt | 13 +- .../java/com/ivy/data/ArbAccountEntity.kt | 2 +- .../java/com/ivy/data/ArbTransactionEntity.kt | 4 +- .../ivy/data/backup/BackupDataUseCaseTest.kt | 72 +- .../com/ivy/data/dao/FakeAccountDaoTest.kt | 26 - .../com/ivy/data/dao/FakeCategoryDaoTest.kt | 44 - ...ryImplTest.kt => AccountRepositoryTest.kt} | 41 +- ...yImplTest.kt => CategoryRepositoryTest.kt} | 30 +- ...Test.kt => ExchangeRatesRepositoryTest.kt} | 7 +- ...plTest.kt => TransactionRepositoryTest.kt} | 45 +- .../repository/mapper/AccountMapperTest.kt | 30 +- .../repository/mapper/CategoryMapperTest.kt | 20 +- .../mapper/TransactionMapperTest.kt | 165 ++-- .../com/ivy/data/model/testing/ArbAccount.kt | 4 - .../com/ivy/data/model/testing/ArbCategory.kt | 4 - .../ivy/data/model/testing/ArbTransaction.kt | 9 - .../data/model/testing/ArbTransactionTest.kt | 9 - .../main/kotlin/com/ivy/data/model/Account.kt | 7 +- .../kotlin/com/ivy/data/model/Category.kt | 7 +- .../src/main/kotlin/com/ivy/data/model/Tag.kt | 10 +- .../kotlin/com/ivy/data/model/Transaction.kt | 10 +- .../com/ivy/data/model/sync/Identifiable.kt | 5 + .../com/ivy/data/model/sync/Syncable.kt | 13 - .../data/model/primitive/TransactionTest.kt | 8 +- .../usecase/SyncExchangeRatesUseCaseTest.kt | 3 +- .../csv/ExportCsvUseCasePropertyTest.kt | 4 +- .../java/com/ivy/legacy/datamodel/Account.kt | 3 - .../action/wallet/CalcWalletBalanceAct.kt | 3 - .../domain/deprecated/logic/AccountCreator.kt | 3 - .../domain/deprecated/logic/BudgetCreator.kt | 2 +- .../deprecated/logic/CategoryCreator.kt | 3 - .../domain/deprecated/logic/LoanCreator.kt | 2 +- .../deprecated/logic/LoanRecordCreator.kt | 2 +- .../logic/PlannedPaymentsGenerator.kt | 2 +- .../deprecated/logic/PlannedPaymentsLogic.kt | 8 +- .../deprecated/logic/PreloadDataLogic.kt | 3 - .../deprecated/logic/csv/CSVImporter.kt | 3 - .../loantrasactions/LoanTransactionsCore.kt | 3 - .../theme/modal/edit/ChooseCategoryModal.kt | 7 - .../component/transaction/TransactionCard.kt | 14 +- 109 files changed, 2202 insertions(+), 1775 deletions(-) create mode 100644 shared/data/core/schemas/com.ivy.data.db.IvyRoomDatabase/129.json create mode 100644 shared/data/core/src/androidTest/java/com/ivy/data/db/Migration128to129Test.kt create mode 100644 shared/data/core/src/main/java/com/ivy/data/db/migration/Migration128to129_DeleteIsDeleted.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeCurrencyRepository.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/CurrencyRepositoryImpl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImpl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/TagRepositoryImpl.kt delete mode 100644 shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt rename shared/data/core/src/test/java/com/ivy/data/repository/{impl/AccountRepositoryImplTest.kt => AccountRepositoryTest.kt} (90%) rename shared/data/core/src/test/java/com/ivy/data/repository/{impl/CategoryRepositoryImplTest.kt => CategoryRepositoryTest.kt} (90%) rename shared/data/core/src/test/java/com/ivy/data/repository/{impl/ExchangeRatesRepositoryImplTest.kt => ExchangeRatesRepositoryTest.kt} (93%) rename shared/data/core/src/test/java/com/ivy/data/repository/{impl/TransactionRepositoryImplTest.kt => TransactionRepositoryTest.kt} (92%) create mode 100644 shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Identifiable.kt delete mode 100644 shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Syncable.kt diff --git a/.gitignore b/.gitignore index 24becfd9ad..d0da9b341f 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,8 @@ captures/ .idea/codeStyles .idea/kotlinc.xml .DS_Store +.idea/deploymentTargetSelector.xml +.idea/other.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. @@ -98,3 +100,6 @@ lint/reports/ # JS node_modules/* /ci-actions/compose-stability/ivy-compose-stability-report.txt + +# Kotlin Compiler +.kotlin/sessions/* diff --git a/screen/accounts/src/main/java/com/ivy/accounts/AccountsTab.kt b/screen/accounts/src/main/java/com/ivy/accounts/AccountsTab.kt index 5d8af2a099..99799d431b 100644 --- a/screen/accounts/src/main/java/com/ivy/accounts/AccountsTab.kt +++ b/screen/accounts/src/main/java/com/ivy/accounts/AccountsTab.kt @@ -39,6 +39,7 @@ import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.legacy.IvyWalletPreview +import com.ivy.legacy.data.model.AccountData import com.ivy.legacy.utils.clickableNoIndication import com.ivy.legacy.utils.horizontalSwipeListener import com.ivy.legacy.utils.rememberInteractionSource @@ -59,7 +60,6 @@ import com.ivy.wallet.ui.theme.dynamicContrast import com.ivy.wallet.ui.theme.findContrastTextColor import com.ivy.wallet.ui.theme.toComposeColor import kotlinx.collections.immutable.persistentListOf -import java.time.Instant import java.util.UUID @Composable @@ -201,7 +201,7 @@ private fun BoxWithConstraintsScope.UI( @Composable private fun AccountCard( baseCurrency: String, - accountData: com.ivy.legacy.data.model.AccountData, + accountData: AccountData, onBalanceClick: () -> Unit, onClick: () -> Unit ) { @@ -244,7 +244,7 @@ private fun AccountCard( @Composable private fun AccountHeader( - accountData: com.ivy.legacy.data.model.AccountData, + accountData: AccountData, currency: String, baseCurrency: String, contrastColor: Color, @@ -341,8 +341,6 @@ private fun PreviewAccountsTab(theme: Theme = Theme.LIGHT) { icon = null, includeInBalance = true, orderNum = 0.0, - lastUpdated = Instant.EPOCH, - removed = false ) val acc2 = com.ivy.data.model.Account( @@ -353,8 +351,6 @@ private fun PreviewAccountsTab(theme: Theme = Theme.LIGHT) { icon = null, includeInBalance = true, orderNum = 0.0, - lastUpdated = Instant.EPOCH, - removed = false ) val acc3 = com.ivy.data.model.Account( @@ -365,8 +361,6 @@ private fun PreviewAccountsTab(theme: Theme = Theme.LIGHT) { icon = IconAsset.unsafe("revolut"), includeInBalance = true, orderNum = 0.0, - lastUpdated = Instant.EPOCH, - removed = false ) val acc4 = com.ivy.data.model.Account( @@ -377,34 +371,32 @@ private fun PreviewAccountsTab(theme: Theme = Theme.LIGHT) { icon = IconAsset.unsafe("cash"), includeInBalance = true, orderNum = 0.0, - lastUpdated = Instant.EPOCH, - removed = false ) val state = AccountsState( baseCurrency = "BGN", accountsData = persistentListOf( - com.ivy.legacy.data.model.AccountData( + AccountData( account = acc1, balance = 2125.0, balanceBaseCurrency = null, monthlyExpenses = 920.0, monthlyIncome = 3045.0 ), - com.ivy.legacy.data.model.AccountData( + AccountData( account = acc2, balance = 12125.21, balanceBaseCurrency = null, monthlyExpenses = 1350.50, monthlyIncome = 8000.48 ), - com.ivy.legacy.data.model.AccountData( + AccountData( account = acc3, balance = 1200.0, balanceBaseCurrency = 1979.64, monthlyExpenses = 750.0, monthlyIncome = 1000.30 ), - com.ivy.legacy.data.model.AccountData( + AccountData( account = acc4, balance = 820.0, balanceBaseCurrency = null, diff --git a/screen/budgets/src/main/java/com/ivy/budgets/BudgetModal.kt b/screen/budgets/src/main/java/com/ivy/budgets/BudgetModal.kt index 620686eb78..be96c67cc5 100644 --- a/screen/budgets/src/main/java/com/ivy/budgets/BudgetModal.kt +++ b/screen/budgets/src/main/java/com/ivy/budgets/BudgetModal.kt @@ -50,7 +50,6 @@ import com.ivy.wallet.ui.theme.modal.ModalDelete import com.ivy.wallet.ui.theme.modal.ModalTitle import com.ivy.wallet.ui.theme.modal.edit.AmountModal import com.ivy.wallet.ui.theme.toComposeColor -import java.time.Instant import java.util.UUID @Deprecated("Old design system. Use `:ivy-design` and Material3") @@ -274,9 +273,7 @@ private fun Preview_create() { color = ColorInt(Purple1Dark.toArgb()), icon = IconAsset.unsafe("atom"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) BudgetModal( @@ -290,18 +287,14 @@ private fun Preview_create() { color = ColorInt(Red3Light.toArgb()), icon = IconAsset.unsafe("pet"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Home"), color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), ), accounts = emptyList() @@ -323,9 +316,7 @@ private fun Preview_edit() { color = ColorInt(Purple1Dark.toArgb()), icon = IconAsset.unsafe("atom"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) BudgetModal( @@ -345,18 +336,14 @@ private fun Preview_edit() { color = ColorInt(Red3Light.toArgb()), icon = IconAsset.unsafe("pet"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Home"), color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), ), accounts = emptyList() diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt index d8ea6d1109..f683f16204 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt @@ -78,7 +78,6 @@ import com.ivy.wallet.ui.theme.toComposeColor import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import java.time.Instant import java.util.UUID @Composable @@ -568,9 +567,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { name = NotBlankTrimmedString.unsafe("Groceries"), color = ColorInt(Green.toArgb()), icon = IconAsset.unsafe("groceries"), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false ), monthlyBalance = 2125.0, monthlyExpenses = 920.0, @@ -582,9 +579,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { name = NotBlankTrimmedString.unsafe("Fun"), color = ColorInt(Orange.toArgb()), icon = IconAsset.unsafe("game"), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false ), monthlyBalance = 1200.0, monthlyExpenses = 750.0, @@ -596,9 +591,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { name = NotBlankTrimmedString.unsafe("Ivy"), color = ColorInt(IvyDark.toArgb()), icon = IconAsset.unsafe("star"), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false ), monthlyBalance = 1200.0, monthlyExpenses = 0.0, @@ -610,9 +603,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { name = NotBlankTrimmedString.unsafe("Food"), color = ColorInt(GreenLight.toArgb()), icon = IconAsset.unsafe("atom"), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false ), monthlyBalance = 12125.21, monthlyExpenses = 1350.50, @@ -624,9 +615,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { name = NotBlankTrimmedString.unsafe("Shisha"), color = ColorInt(GreenDark.toArgb()), icon = IconAsset.unsafe("drink"), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false ), monthlyBalance = 820.0, monthlyExpenses = 340.0, diff --git a/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt b/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt index 72ad53e5c7..63d31d5941 100644 --- a/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt +++ b/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt @@ -31,7 +31,6 @@ import com.ivy.wallet.domain.pure.util.nextOrderNum import com.ivy.wallet.ui.theme.Green import com.ivy.wallet.ui.theme.IvyDark import kotlinx.collections.immutable.toImmutableList -import java.time.Instant import java.util.UUID import javax.inject.Inject import kotlin.math.absoluteValue @@ -334,8 +333,6 @@ class CSVImporterV2 @Inject constructor( color = ColorInt(colorArgb), icon = icon?.let(IconAsset::from)?.getOrNull(), orderNum = orderNum ?: categoryRepository.findMaxOrderNum().nextOrderNum(), - lastUpdated = Instant.EPOCH, - removed = false ) }.getOrNull() diff --git a/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingCategories.kt b/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingCategories.kt index 45fee435bf..9bf5b7274a 100644 --- a/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingCategories.kt +++ b/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingCategories.kt @@ -64,7 +64,6 @@ import com.ivy.wallet.ui.theme.findContrastTextColor import com.ivy.wallet.ui.theme.modal.edit.CategoryModal import com.ivy.wallet.ui.theme.modal.edit.CategoryModalData import com.ivy.wallet.ui.theme.toComposeColor -import java.time.Instant import java.util.UUID @ExperimentalFoundationApi @@ -404,9 +403,7 @@ private fun Preview_Categories() { color = ColorInt(Orange.toArgb()), icon = IconAsset.unsafe("fooddrinks"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) ) ) @@ -480,9 +477,7 @@ private fun Preview_Premium() { color = ColorInt(Orange.toArgb()), icon = IconAsset.unsafe("fooddrinks"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) } ) diff --git a/screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt b/screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt index 661bf39377..3dee6a70e6 100644 --- a/screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt +++ b/screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt @@ -8,7 +8,13 @@ import android.graphics.RectF import android.view.MotionEvent import android.view.View import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,12 +35,17 @@ import com.ivy.design.l0_system.UI import com.ivy.legacy.utils.drawColoredShadow import com.ivy.legacy.utils.timeNowUTC import com.ivy.ui.R -import com.ivy.wallet.ui.theme.* +import com.ivy.wallet.ui.theme.Black +import com.ivy.wallet.ui.theme.Gradient +import com.ivy.wallet.ui.theme.Gray +import com.ivy.wallet.ui.theme.Green +import com.ivy.wallet.ui.theme.IvyDark +import com.ivy.wallet.ui.theme.RedLight import com.ivy.wallet.ui.theme.components.IvyIcon +import com.ivy.wallet.ui.theme.toComposeColor import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import timber.log.Timber -import java.time.Instant import java.util.UUID import kotlin.math.acos import kotlin.math.sqrt @@ -284,9 +295,7 @@ private fun Preview() { color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 791.0 ), @@ -296,9 +305,7 @@ private fun Preview() { color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 411.93 ), @@ -308,9 +315,7 @@ private fun Preview() { color = ColorInt(IvyDark.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 260.03 ), @@ -320,9 +325,7 @@ private fun Preview() { color = ColorInt(RedLight.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 160.0 ), diff --git a/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticScreen.kt b/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticScreen.kt index 34ab624fde..901cd45521 100644 --- a/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticScreen.kt +++ b/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticScreen.kt @@ -46,11 +46,11 @@ import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.design.utils.thenIf import com.ivy.legacy.utils.drawColoredShadow import com.ivy.legacy.utils.format import com.ivy.legacy.utils.horizontalSwipeListener import com.ivy.legacy.utils.rememberSwipeListenerState -import com.ivy.design.utils.thenIf import com.ivy.navigation.EditTransactionScreen import com.ivy.navigation.PieChartStatisticScreen import com.ivy.navigation.TransactionsScreen @@ -80,7 +80,6 @@ import com.ivy.wallet.ui.theme.pureBlur import com.ivy.wallet.ui.theme.toComposeColor import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1Row import kotlinx.collections.immutable.persistentListOf -import java.time.Instant import java.util.UUID @ExperimentalFoundationApi @@ -482,9 +481,7 @@ private fun Preview_Expense() { color = ColorInt(Green.toArgb()), icon = IconAsset.unsafe("bills"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 791.0 ), @@ -499,9 +496,7 @@ private fun Preview_Expense() { color = ColorInt(Orange.toArgb()), icon = IconAsset.unsafe("trees"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 411.93 ), @@ -511,9 +506,7 @@ private fun Preview_Expense() { color = ColorInt(IvyDark.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 260.03 ), @@ -523,9 +516,7 @@ private fun Preview_Expense() { color = ColorInt(RedLight.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 160.0 ), @@ -535,9 +526,7 @@ private fun Preview_Expense() { color = ColorInt(Red.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 2.0 ), @@ -547,9 +536,7 @@ private fun Preview_Expense() { color = ColorInt(IvyLight.toArgb()), icon = IconAsset.unsafe("work"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 2.0 ), @@ -585,9 +572,7 @@ private fun Preview_Income() { color = ColorInt(Green.toArgb()), icon = IconAsset.unsafe("bills"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 791.0 ), @@ -602,9 +587,7 @@ private fun Preview_Income() { color = ColorInt(Orange.toArgb()), icon = IconAsset.unsafe("trees"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 411.93 ), @@ -614,9 +597,7 @@ private fun Preview_Income() { color = ColorInt(IvyDark.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 260.03 ), @@ -626,9 +607,7 @@ private fun Preview_Income() { color = ColorInt(RedLight.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 160.0 ), @@ -638,9 +617,7 @@ private fun Preview_Income() { color = ColorInt(Red.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 2.0 ), @@ -650,9 +627,7 @@ private fun Preview_Income() { color = ColorInt(IvyLight.toArgb()), icon = IconAsset.unsafe("work"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), amount = 2.0 ), diff --git a/screen/piechart/src/main/java/com/ivy/piechart/action/PieChartAct.kt b/screen/piechart/src/main/java/com/ivy/piechart/action/PieChartAct.kt index a26ede482e..f81f25a91b 100644 --- a/screen/piechart/src/main/java/com/ivy/piechart/action/PieChartAct.kt +++ b/screen/piechart/src/main/java/com/ivy/piechart/action/PieChartAct.kt @@ -29,7 +29,6 @@ import com.ivy.wallet.domain.pure.data.IncomeExpenseTransferPair import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal -import java.time.Instant import java.util.UUID import javax.inject.Inject @@ -47,9 +46,7 @@ class PieChartAct @Inject constructor( color = ColorInt(RedLight.toArgb()), icon = IconAsset.unsafe("transfer"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) override suspend fun Input.compose(): suspend () -> Output = suspend { diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt index c79c7a3b43..3917f99891 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt @@ -467,7 +467,7 @@ class EditPlannedViewModel @Inject constructor( ioThread { loadedRule?.let { plannedPaymentRuleWriter.deleteById(it.id) - transactionRepository.flagDeletedByRecurringRuleIdAndNoDateTime( + transactionRepository.deletedByRecurringRuleIdAndNoDateTime( recurringRuleId = it.id ) } diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt b/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt index 1568b6845b..c9fc110e41 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentCard.kt @@ -56,7 +56,6 @@ import com.ivy.wallet.ui.theme.findContrastTextColor import com.ivy.wallet.ui.theme.toComposeColor import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import java.time.Instant import java.time.LocalDateTime import java.util.UUID @@ -275,9 +274,7 @@ private fun Preview_oneTime() { color = ColorInt(Blue.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { @@ -315,9 +312,7 @@ private fun Preview_recurring() { color = ColorInt(Orange.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { @@ -355,9 +350,7 @@ private fun Preview_recurringError() { color = ColorInt(Orange.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt b/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt index 95bc584d34..f666a92826 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.base.model.TransactionType -import com.ivy.ui.rememberScrollPositionListState import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import com.ivy.data.model.IntervalType @@ -31,10 +30,10 @@ import com.ivy.navigation.PlannedPaymentsScreen import com.ivy.navigation.navigation import com.ivy.navigation.screenScopedViewModel import com.ivy.ui.R +import com.ivy.ui.rememberScrollPositionListState import com.ivy.wallet.ui.theme.Green import com.ivy.wallet.ui.theme.Orange import kotlinx.collections.immutable.persistentListOf -import java.time.Instant import java.util.UUID @Composable @@ -114,18 +113,14 @@ private fun Preview() { color = ColorInt(Purple.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) val shisha = Category( name = NotBlankTrimmedString.unsafe("Shisha"), color = ColorInt(Orange.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) UI( diff --git a/screen/reports/src/main/java/com/ivy/reports/FilterOverlay.kt b/screen/reports/src/main/java/com/ivy/reports/FilterOverlay.kt index 807f27e7b8..3761b13da8 100644 --- a/screen/reports/src/main/java/com/ivy/reports/FilterOverlay.kt +++ b/screen/reports/src/main/java/com/ivy/reports/FilterOverlay.kt @@ -40,11 +40,11 @@ import androidx.compose.ui.zIndex import com.ivy.base.model.TransactionType import com.ivy.data.model.Category import com.ivy.data.model.CategoryId +import com.ivy.data.model.Tag +import com.ivy.data.model.TagId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.model.Tag -import com.ivy.data.model.TagId import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.domain.legacy.ui.theme.components.ListItem @@ -82,7 +82,6 @@ import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1Row import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.time.Instant import java.util.UUID import kotlin.math.roundToInt @@ -990,9 +989,7 @@ private fun Preview() { color = ColorInt(Purple1Dark.toArgb()), icon = IconAsset.unsafe("atom"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) FilterOverlay( @@ -1012,18 +1009,14 @@ private fun Preview() { color = ColorInt(Red3Light.toArgb()), icon = IconAsset.unsafe("pet"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Home"), color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), ), diff --git a/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt b/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt index 61f77eb2dc..43aa4ae264 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt @@ -70,7 +70,6 @@ import com.ivy.wallet.ui.theme.components.IvyToolbar import com.ivy.wallet.ui.theme.pureBlur import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.time.Instant import java.util.UUID @ExperimentalFoundationApi @@ -412,9 +411,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { color = ColorInt(Purple1Dark.toArgb()), icon = IconAsset.unsafe("atom"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) val state = ReportScreenState( baseCurrency = "BGN", @@ -447,18 +444,14 @@ private fun Preview(theme: Theme = Theme.LIGHT) { color = ColorInt(Red3Light.toArgb()), icon = IconAsset.unsafe("pet"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Home"), color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), ), ) @@ -479,9 +472,7 @@ private fun Preview_NO_FILTER(theme: Theme = Theme.LIGHT) { color = ColorInt(Purple1Dark.toArgb()), icon = IconAsset.unsafe("atom"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) val state = ReportScreenState( baseCurrency = "BGN", @@ -516,18 +507,14 @@ private fun Preview_NO_FILTER(theme: Theme = Theme.LIGHT) { color = ColorInt(Red3Light.toArgb()), icon = IconAsset.unsafe("pet"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Home"), color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), ), ) diff --git a/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt b/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt index 52ce5cfa01..8ca2bba704 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt @@ -64,7 +64,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal -import java.time.Instant import java.time.ZoneId import java.util.UUID import javax.inject.Inject @@ -91,9 +90,7 @@ class ReportViewModel @Inject constructor( color = ColorInt(Gray.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) private val baseCurrency = mutableStateOf("") private val categories = mutableStateOf>(persistentListOf()) diff --git a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt index 12812e1b5b..c642b22a9d 100644 --- a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt +++ b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt @@ -62,7 +62,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch -import java.time.Instant import java.util.UUID import javax.inject.Inject import com.ivy.legacy.datamodel.Account as LegacyAccount @@ -627,9 +626,7 @@ class TransactionsViewModel @Inject constructor( color = ColorInt(RedLight.toArgb()), icon = IconAsset.unsafe("transfer"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) category.value = accountTransferCategory val accountFilterIdSet = accountFilterList.toHashSet() @@ -728,8 +725,8 @@ class TransactionsViewModel @Inject constructor( private suspend fun deleteAccount(accountId: UUID) { ioThread { - transactionRepository.flagDeletedByAccountId(accountId = accountId) - plannedPaymentRuleWriter.flagDeletedByAccountId(accountId = accountId) + transactionRepository.deleteAllByAccountId(accountId = AccountId(accountId)) + plannedPaymentRuleWriter.deletedByAccountId(accountId = accountId) accountRepository.deleteById(AccountId(accountId)) nav.back() @@ -738,7 +735,7 @@ class TransactionsViewModel @Inject constructor( private suspend fun deleteCategory(categoryId: UUID) { ioThread { - categoryWriter.flagDeleted(categoryId) + categoryWriter.deleteById(categoryId) categoryRepository.deleteById(CategoryId(categoryId)) nav.back() diff --git a/shared/data/core/schemas/com.ivy.data.db.IvyRoomDatabase/129.json b/shared/data/core/schemas/com.ivy.data.db.IvyRoomDatabase/129.json new file mode 100644 index 0000000000..c4f9962c2b --- /dev/null +++ b/shared/data/core/schemas/com.ivy.data.db.IvyRoomDatabase/129.json @@ -0,0 +1,838 @@ +{ + "formatVersion": 1, + "database": { + "version": 129, + "identityHash": "177f8030f5202e9229cc552c4dcb9f81", + "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, `paidForDateTime` INTEGER, `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": "paidForDateTime", + "columnName": "paidForDateTime", + "affinity": "INTEGER", + "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, `dateTime` INTEGER NOT NULL, `isDeleted` 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": "dateTime", + "columnName": "dateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "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, '177f8030f5202e9229cc552c4dcb9f81')" + ] + } +} \ No newline at end of file diff --git a/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt b/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt index 38c828596b..b60bc9086d 100644 --- a/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt +++ b/shared/data/core/src/androidTest/java/com/ivy/data/backup/BackupDataUseCaseAndroidTest.kt @@ -13,8 +13,9 @@ import com.ivy.base.legacy.SharedPrefs import com.ivy.data.DataObserver import com.ivy.data.db.IvyRoomDatabase import com.ivy.data.file.FileSystem -import com.ivy.data.repository.fake.FakeAccountRepository -import com.ivy.data.repository.fake.FakeCurrencyRepository +import com.ivy.data.repository.AccountRepository +import com.ivy.data.repository.CurrencyRepository +import com.ivy.data.repository.fake.fakeRepositoryMemoFactory import com.ivy.data.repository.mapper.AccountMapper import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.ints.shouldBeGreaterThan @@ -36,6 +37,13 @@ class BackupDataUseCaseAndroidTest { val context = ApplicationProvider.getApplicationContext() db = Room.inMemoryDatabaseBuilder(context, IvyRoomDatabase::class.java).build() val appContext = InstrumentationRegistry.getInstrumentation().context + val accountMapper = AccountMapper( + currencyRepository = CurrencyRepository( + settingsDao = db.settingsDao, + writeSettingsDao = db.writeSettingsDao, + dispatchersProvider = TestDispatchersProvider, + ) + ) useCase = BackupDataUseCase( accountDao = db.accountDao, budgetDao = db.budgetDao, @@ -47,18 +55,14 @@ class BackupDataUseCaseAndroidTest { transactionDao = db.transactionDao, transactionWriter = db.writeTransactionDao, sharedPrefs = SharedPrefs(appContext), - accountRepository = FakeAccountRepository( + accountRepository = AccountRepository( accountDao = db.accountDao, writeAccountDao = db.writeAccountDao, - settingsDao = db.settingsDao, - writeSettingsDao = db.writeSettingsDao, - ), - accountMapper = AccountMapper( - FakeCurrencyRepository( - settingsDao = db.settingsDao, - writeSettingsDao = db.writeSettingsDao, - ) + mapper = accountMapper, + dispatchersProvider = TestDispatchersProvider, + memoFactory = fakeRepositoryMemoFactory(), ), + accountMapper = accountMapper, categoryWriter = db.writeCategoryDao, settingsWriter = db.writeSettingsDao, budgetWriter = db.writeBudgetDao, diff --git a/shared/data/core/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/shared/data/core/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index e9b0d7888e..aa83db11e4 100644 --- a/shared/data/core/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/shared/data/core/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -82,7 +82,7 @@ class IvyRoomDatabaseMigrationTest { @Test fun migrate126to127_LoanRecordType() { // given - val loanId = java.util.UUID.randomUUID().toString() + val loanId = 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. @@ -110,12 +110,6 @@ class IvyRoomDatabaseMigrationTest { } // when - helper.runMigrationsAndValidate( - TestDb, - 126, - true, - Migration126to127_LoanRecordType() - ) val newDb = helper.runMigrationsAndValidate( TestDb, 127, diff --git a/shared/data/core/src/androidTest/java/com/ivy/data/db/Migration128to129Test.kt b/shared/data/core/src/androidTest/java/com/ivy/data/db/Migration128to129Test.kt new file mode 100644 index 0000000000..c5b4c36316 --- /dev/null +++ b/shared/data/core/src/androidTest/java/com/ivy/data/db/Migration128to129Test.kt @@ -0,0 +1,297 @@ +package com.ivy.data.db + +import android.database.Cursor +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.platform.app.InstrumentationRegistry +import com.ivy.base.model.TransactionType +import com.ivy.data.db.migration.Migration128to129_DeleteIsDeleted +import com.ivy.data.model.LoanType +import io.kotest.matchers.shouldBe +import org.junit.Rule +import org.junit.Test +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.util.UUID + +class Migration128to129Test { + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + IvyRoomDatabase::class.java, + listOf(IvyRoomDatabase.DeleteSEMigration()), + FrameworkSQLiteOpenHelperFactory() + ) + + private val migration = Migration128to129_DeleteIsDeleted() + + @Test + fun deletesDeletedTransactions() = migrationTestCase( + tableName = "transactions", + dataBeforeMigration = { + insertTransaction( + title = "Trn 1", + isDeleted = false, + ) + insertTransaction( + title = "Trn 2", + isDeleted = true, + ) + insertTransaction( + title = "Trn 3", + isDeleted = true, + ) + }, + dataAfterMigration = { + moveToFirst() shouldBe true + getString(getColumnIndexOrThrow("title")) shouldBe "Trn 1" + moveToNext() shouldBe false + } + ) + + private fun SupportSQLiteDatabase.insertTransaction( + title: String, + isDeleted: Boolean + ) { + val sql = """ + INSERT INTO transactions ( + accountId, type, amount, toAccountId, toAmount, title, + description, dateTime, categoryId, dueDate, recurringRuleId, + paidForDateTime, attachmentUrl, loanId, loanRecordId, isSynced, + isDeleted, id + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + """.trimIndent() + val statement = this.compileStatement(sql) + + val id = UUID.randomUUID() + val accountId = UUID.randomUUID() + val type = TransactionType.INCOME + val amount = 10.0 + val isSynced = true + + statement.bindString(1, accountId.toString()) + statement.bindString(2, type.name) + statement.bindDouble(3, amount) + statement.bindString(4, UUID.randomUUID().toString()) + statement.bindNull(5) + statement.bindString(6, title) + statement.bindNull(7) + statement.bindLong(8, Instant.EPOCH.toEpochMilli()) // Use the correct format for dateTime + statement.bindNull(9) + statement.bindNull(10) + statement.bindNull(11) + statement.bindNull(12) + statement.bindNull(13) + statement.bindNull(14) + statement.bindNull(15) + statement.bindLong(16, if (isSynced) 1 else 0) + statement.bindLong(17, if (isDeleted) 1 else 0) + statement.bindString(18, id.toString()) + + statement.executeInsert() + } + + @Test + fun deleteDeletedAccounts() = migrationTestCase( + tableName = "accounts", + dataBeforeMigration = { + insertAccount( + name = "Acc 1", + isDeleted = true, + ) + insertAccount( + name = "Acc 2", + isDeleted = false, + ) + insertAccount( + name = "Acc 3", + isDeleted = true, + ) + }, + dataAfterMigration = { + moveToFirst() shouldBe true + getString(getColumnIndexOrThrow("name")) shouldBe "Acc 2" + moveToNext() shouldBe false + } + ) + + private fun SupportSQLiteDatabase.insertAccount( + name: String, + isDeleted: Boolean, + ) { + val sql = """ + INSERT INTO accounts ( + name, currency, color, icon, orderNum, includeInBalance, + isSynced, isDeleted, id + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + """.trimIndent() + val statement = this.compileStatement(sql) + + val id = UUID.randomUUID() + val currency = "USD" // Dummy currency + val color = 0xFFFFFF // Dummy color (white) + val icon = "default_icon" // Dummy icon + val orderNum = 1.0 + val includeInBalance = true + val isSynced = true + + statement.bindString(1, name) + statement.bindString(2, currency) + statement.bindLong(3, color.toLong()) + statement.bindString(4, icon) + statement.bindDouble(5, orderNum) + statement.bindLong(6, if (includeInBalance) 1 else 0) + statement.bindLong(7, if (isSynced) 1 else 0) + statement.bindLong(8, if (isDeleted) 1 else 0) + statement.bindString(9, id.toString()) + + statement.executeInsert() + } + + @Test + fun deleteDeletedCategories() = migrationTestCase( + tableName = "categories", + dataBeforeMigration = { + insertCategory( + name = "Category 1", + isDeleted = true, + ) + insertCategory( + name = "Category 2", + isDeleted = false, + ) + }, + dataAfterMigration = { + moveToFirst() shouldBe true + getString(getColumnIndexOrThrow("name")) shouldBe "Category 2" + moveToNext() shouldBe false + } + ) + + private fun SupportSQLiteDatabase.insertCategory( + name: String, + isDeleted: Boolean, + ) { + val sql = """ + INSERT INTO categories ( + name, color, icon, orderNum, isSynced, isDeleted, id + ) VALUES ( + ?, ?, ?, ?, ?, ?, ? + ) + """.trimIndent() + val statement = this.compileStatement(sql) + + val id = UUID.randomUUID() + val color = 0xFFFFFF // Dummy color (white) + val icon = "default_icon" // Dummy icon + val orderNum = 1.0 + val isSynced = true + + statement.bindString(1, name) + statement.bindLong(2, color.toLong()) + statement.bindString(3, icon) + statement.bindDouble(4, orderNum) + statement.bindLong(5, if (isSynced) 1 else 0) + statement.bindLong(6, if (isDeleted) 1 else 0) + statement.bindString(7, id.toString()) + + statement.executeInsert() + } + + @Test + fun deleteDeletedLoans() = migrationTestCase( + tableName = "loans", + dataBeforeMigration = { + insertLoan( + name = "Loan 1", + isDeleted = false, + ) + insertLoan( + name = "Loan 2", + isDeleted = true, + ) + }, + dataAfterMigration = { + moveToFirst() shouldBe true + getString(getColumnIndexOrThrow("name")) shouldBe "Loan 1" + moveToNext() shouldBe false + } + ) + + private fun SupportSQLiteDatabase.insertLoan( + name: String, + isDeleted: Boolean, + type: LoanType = LoanType.LEND, + amount: Double = 10.0, + ) { + val sql = """ + INSERT INTO loans ( + name, amount, type, color, icon, orderNum, accountId, + isSynced, isDeleted, dateTime, id + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + """.trimIndent() + val statement = this.compileStatement(sql) + + val id = UUID.randomUUID() + val color = 0xFFFFFF // Dummy color (white) + val icon = "default_icon" // Dummy icon + val orderNum = 1.0 + val accountId = UUID.randomUUID() // Dummy accountId + val isSynced = true + // Convert LocalDateTime to epoch milli + val dateTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000 + + statement.bindString(1, name) + statement.bindDouble(2, amount) + statement.bindString(3, type.name) + statement.bindLong(4, color.toLong()) + statement.bindString(5, icon) + statement.bindDouble(6, orderNum) + statement.bindString(7, accountId.toString()) + statement.bindLong(8, if (isSynced) 1 else 0) + statement.bindLong(9, if (isDeleted) 1 else 0) + statement.bindLong(10, dateTime) + statement.bindString(11, id.toString()) + + statement.executeInsert() + } + + private fun migrationTestCase( + tableName: String, + dataBeforeMigration: SupportSQLiteDatabase.() -> Unit, + dataAfterMigration: Cursor.() -> Unit, + ) { + // Given + helper.createDatabase(TestDb, 128).apply { + dataBeforeMigration() + close() + } + + // When + val newDb = helper.runMigrationsAndValidate( + TestDb, + 129, + true, + migration, + ) + + // Then + newDb.query("SELECT * FROM $tableName").apply { + dataAfterMigration() + } + newDb.close() + } + + companion object { + private const val TestDb = "migration-test" + } +} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt b/shared/data/core/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt index bec15293ff..24ceb35a52 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt @@ -42,6 +42,7 @@ 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.db.migration.Migration127to128_PaidForDateRecord +import com.ivy.data.db.migration.Migration128to129_DeleteIsDeleted import com.ivy.domain.db.RoomTypeConverters import com.ivy.domain.db.migration.Migration105to106_TrnRecurringRules import com.ivy.domain.db.migration.Migration106to107_Wishlist @@ -76,7 +77,7 @@ import com.ivy.domain.db.migration.Migration125to126_Tags spec = IvyRoomDatabase.DeleteSEMigration::class ) ], - version = 128, + version = 129, exportSchema = true ) @TypeConverters(RoomTypeConverters::class) @@ -131,7 +132,8 @@ abstract class IvyRoomDatabase : RoomDatabase() { Migration124to125_LoanEditDateTime(), Migration125to126_Tags(), Migration126to127_LoanRecordType(), - Migration127to128_PaidForDateRecord() + Migration127to128_PaidForDateRecord(), + Migration128to129_DeleteIsDeleted(), ) @Suppress("SpreadOperator") diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeAccountDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeAccountDao.kt index de456cd5e9..7949c69200 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeAccountDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeAccountDao.kt @@ -30,11 +30,6 @@ class FakeAccountDao : AccountDao, WriteAccountDao { values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - val acc = accounts[id] ?: return - accounts[id] = acc.copy(isDeleted = true) - } - override suspend fun deleteById(id: UUID) { accounts.remove(id) } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeBudgetDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeBudgetDao.kt index 8df7465bdc..84bd75ff25 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeBudgetDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeBudgetDao.kt @@ -37,10 +37,6 @@ class FakeBudgetDao : BudgetDao, WriteBudgetDao { values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - TODO("Not yet implemented") - } - override suspend fun deleteById(id: UUID) { items.removeIf { it.id == id } } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt index 0d24cb0ba3..f2cd0f1df6 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt @@ -35,16 +35,6 @@ class FakeCategoryDao : CategoryDao, WriteCategoryDao { values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - items.replaceAll { categoryEntity -> - if (categoryEntity.id == id) { - categoryEntity.copy(isDeleted = true) - } else { - categoryEntity - } - } - } - override suspend fun deleteById(id: UUID) { items.removeIf { it.id == id } } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanDao.kt index 1581c0dd03..19045af5d3 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanDao.kt @@ -37,10 +37,6 @@ class FakeLoanDao : LoanDao, WriteLoanDao { values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - TODO("Not yet implemented") - } - override suspend fun deleteById(id: UUID) { items.removeIf { it.id == id } } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanRecordDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanRecordDao.kt index eaae061f17..66d885956f 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanRecordDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeLoanRecordDao.kt @@ -37,10 +37,6 @@ class FakeLoanRecordDao : LoanRecordDao, WriteLoanRecordDao { values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - TODO("Not yet implemented") - } - override suspend fun deleteById(id: UUID) { items.removeIf { it.id == id } } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakePlannedPaymentDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakePlannedPaymentDao.kt index 374a21c9d2..e005c8213e 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakePlannedPaymentDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakePlannedPaymentDao.kt @@ -41,12 +41,8 @@ class FakePlannedPaymentDao : PlannedPaymentRuleDao, WritePlannedPaymentRuleDao values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - TODO("Not yet implemented") - } - - override suspend fun flagDeletedByAccountId(accountId: UUID) { - TODO("Not yet implemented") + override suspend fun deletedByAccountId(accountId: UUID) { + items.removeIf { it.accountId == accountId } } override suspend fun deleteById(id: UUID) { diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt index e6465635a9..dcdd82fb05 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt @@ -309,34 +309,8 @@ class FakeTransactionDao : TransactionDao, WriteTransactionDao { values.forEach { save(it) } } - override suspend fun flagDeleted(id: UUID) { - items.replaceAll { - if (it.id == id) { - it.copy(isDeleted = true) - } else { - it - } - } - } - - override suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) { - items.replaceAll { - if (it.recurringRuleId == recurringRuleId && it.dateTime == null) { - it.copy(isDeleted = true) - } else { - it - } - } - } - - override suspend fun flagDeletedByAccountId(accountId: UUID) { - items.replaceAll { - if (it.accountId == accountId) { - it.copy(isDeleted = true) - } else { - it - } - } + override suspend fun deletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) { + items.removeIf { it.recurringRuleId == recurringRuleId } } override suspend fun deleteById(id: UUID) { diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteAccountDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteAccountDao.kt index 81016c2c2b..971244730f 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteAccountDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteAccountDao.kt @@ -14,9 +14,6 @@ interface WriteAccountDao { @Upsert suspend fun saveMany(values: List) - @Query("UPDATE accounts SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - @Query("DELETE FROM accounts WHERE id = :id") suspend fun deleteById(id: UUID) diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteBudgetDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteBudgetDao.kt index fd594893dd..0424cc7cfe 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteBudgetDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteBudgetDao.kt @@ -17,9 +17,6 @@ interface WriteBudgetDao { @Query("DELETE FROM budgets WHERE id = :id") suspend fun deleteById(id: UUID) - @Query("UPDATE budgets SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - @Query("DELETE FROM budgets") suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteCategoryDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteCategoryDao.kt index f83209c371..5eb7afed6b 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteCategoryDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteCategoryDao.kt @@ -17,9 +17,6 @@ interface WriteCategoryDao { @Query("DELETE FROM categories WHERE id = :id") suspend fun deleteById(id: UUID) - @Query("UPDATE categories SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - @Query("DELETE FROM categories") suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanDao.kt index 5936dc4328..748886dc92 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanDao.kt @@ -17,9 +17,6 @@ interface WriteLoanDao { @Query("DELETE FROM loans WHERE id = :id") suspend fun deleteById(id: UUID) - @Query("UPDATE loans SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - @Query("DELETE FROM loans") suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanRecordDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanRecordDao.kt index a2e3627894..93d488c91b 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanRecordDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteLoanRecordDao.kt @@ -17,9 +17,6 @@ interface WriteLoanRecordDao { @Query("DELETE FROM loan_records WHERE id = :id") suspend fun deleteById(id: UUID) - @Query("UPDATE loan_records SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - @Query("DELETE FROM loan_records") suspend fun deleteAll() } diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WritePlannedPaymentRuleDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WritePlannedPaymentRuleDao.kt index 77ba5ea4ae..438339f943 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WritePlannedPaymentRuleDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WritePlannedPaymentRuleDao.kt @@ -14,11 +14,8 @@ interface WritePlannedPaymentRuleDao { @Upsert suspend fun saveMany(value: List) - @Query("UPDATE planned_payment_rules SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - @Query("UPDATE planned_payment_rules SET isDeleted = 1, isSynced = 0 WHERE accountId = :accountId") - suspend fun flagDeletedByAccountId(accountId: UUID) + suspend fun deletedByAccountId(accountId: UUID) @Query("DELETE FROM planned_payment_rules WHERE id = :id") suspend fun deleteById(id: UUID) diff --git a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteTransactionDao.kt b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteTransactionDao.kt index 06b916ac20..6079fbe6aa 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteTransactionDao.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/dao/write/WriteTransactionDao.kt @@ -14,17 +14,8 @@ interface WriteTransactionDao { @Upsert suspend fun saveMany(value: List) - @Query("UPDATE transactions SET isDeleted = 1, isSynced = 0 WHERE id = :id") - suspend fun flagDeleted(id: UUID) - - @Query( - "UPDATE transactions SET isDeleted = 1, isSynced = 0 WHERE" + - " recurringRuleId = :recurringRuleId AND dateTime IS NULL" - ) - suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) - - @Query("UPDATE transactions SET isDeleted = 1, isSynced = 0 WHERE accountId = :accountId") - suspend fun flagDeletedByAccountId(accountId: UUID) + @Query("DELETE FROM transactions WHERE recurringRuleId = :recurringRuleId AND dateTime IS NULL") + suspend fun deletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) @Query("DELETE FROM transactions WHERE id = :id") suspend fun deleteById(id: UUID) diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/AccountEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/AccountEntity.kt index a7ba50974d..b9e0277c1e 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/AccountEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/AccountEntity.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "accounts") @@ -25,8 +26,10 @@ data class AccountEntity( @SerialName("includeInBalance") val includeInBalance: Boolean = true, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/BudgetEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/BudgetEntity.kt index c1817c97ee..3c68a8be6d 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/BudgetEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/BudgetEntity.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "budgets") @@ -22,8 +23,10 @@ data class BudgetEntity( @SerialName("accountIdsSerialized") val accountIdsSerialized: String?, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/CategoryEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/CategoryEntity.kt index ff0040c3e5..f3bb05f617 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/CategoryEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/CategoryEntity.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "categories") @@ -21,8 +22,10 @@ data class CategoryEntity( @SerialName("orderNum") val orderNum: Double = 0.0, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanEntity.kt index b95476972d..5d635a35ed 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanEntity.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable import java.time.LocalDateTime import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "loans") @@ -31,10 +32,13 @@ data class LoanEntity( @Serializable(with = KSerializerUUID::class) val accountId: UUID? = null, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, + @SerialName("dateTime") @Serializable(with = KSerializerLocalDateTime::class) val dateTime: LocalDateTime? = null, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt index fce9b71d94..3dcb3d3097 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt @@ -39,8 +39,10 @@ data class LoanRecordEntity( @SerialName("loanRecordType") val loanRecordType: LoanRecordType = LoanRecordType.DECREASE, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/PlannedPaymentRuleEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/PlannedPaymentRuleEntity.kt index 680c27eae0..18d56e7599 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/PlannedPaymentRuleEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/PlannedPaymentRuleEntity.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.Serializable import java.time.LocalDateTime import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "planned_payment_rules") @@ -40,8 +41,10 @@ data class PlannedPaymentRuleEntity( @SerialName("description") val description: String? = null, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/SettingsEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/SettingsEntity.kt index 1c4a55acdf..b7507bb1f0 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/SettingsEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/SettingsEntity.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.util.* +@Suppress("DataClassDefaultValues") @Deprecated("Legacy concept - migrate to DataStore and get rid of it.") @Keep @Serializable @@ -23,8 +24,10 @@ data class SettingsEntity( @SerialName("name") val name: String, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/TagAssociationEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/TagAssociationEntity.kt index 03c19cf316..6e45555c40 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/TagAssociationEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/TagAssociationEntity.kt @@ -21,10 +21,12 @@ data class TagAssociationEntity( @Serializable(with = KSerializerUUID::class) val associatedId: UUID, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("lastSyncTime") @Serializable(with = KSerializerInstant::class) val lastSyncedTime: Instant, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean ) \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/TagEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/TagEntity.kt index e0fa2148ec..36af0d8cea 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/TagEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/TagEntity.kt @@ -31,13 +31,15 @@ data class TagEntity( @SerialName("orderNum") val orderNum: Double, - @SerialName("isDeleted") - val isDeleted: Boolean, - @SerialName("creationTime") @Serializable(with = KSerializerInstant::class) val dateTime: Instant, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") + @SerialName("isDeleted") + val isDeleted: Boolean, + + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("lastSyncTime") @Serializable(with = KSerializerInstant::class) val lastSyncedTime: Instant diff --git a/shared/data/core/src/main/java/com/ivy/data/db/entity/TransactionEntity.kt b/shared/data/core/src/main/java/com/ivy/data/db/entity/TransactionEntity.kt index 473d96f93b..273b13487b 100644 --- a/shared/data/core/src/main/java/com/ivy/data/db/entity/TransactionEntity.kt +++ b/shared/data/core/src/main/java/com/ivy/data/db/entity/TransactionEntity.kt @@ -57,8 +57,10 @@ data class TransactionEntity( @SerialName("loanRecordId") @Serializable(with = KSerializerUUID::class) val loanRecordId: UUID? = null, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isSynced") val isSynced: Boolean = false, + @Deprecated("Obsolete field used for cloud sync. Can't be deleted because of backwards compatibility") @SerialName("isDeleted") val isDeleted: Boolean = false, diff --git a/shared/data/core/src/main/java/com/ivy/data/db/migration/Migration128to129_DeleteIsDeleted.kt b/shared/data/core/src/main/java/com/ivy/data/db/migration/Migration128to129_DeleteIsDeleted.kt new file mode 100644 index 0000000000..79545e3d01 --- /dev/null +++ b/shared/data/core/src/main/java/com/ivy/data/db/migration/Migration128to129_DeleteIsDeleted.kt @@ -0,0 +1,26 @@ +package com.ivy.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +@Suppress("MagicNumber", "ClassNaming") +class Migration128to129_DeleteIsDeleted : Migration(128, 129) { + override fun migrate(database: SupportSQLiteDatabase) { + with(database) { + deleteDeletedFrom(tableName = "accounts") + deleteDeletedFrom(tableName = "budgets") + deleteDeletedFrom(tableName = "categories") + deleteDeletedFrom(tableName = "loan_records") + deleteDeletedFrom(tableName = "loans") + deleteDeletedFrom(tableName = "planned_payment_rules") + deleteDeletedFrom(tableName = "settings") + deleteDeletedFrom(tableName = "tags") + deleteDeletedFrom(tableName = "tags_association") + deleteDeletedFrom(tableName = "transactions") + } + } + + private fun SupportSQLiteDatabase.deleteDeletedFrom(tableName: String) { + execSQL("DELETE FROM $tableName WHERE isDeleted = 1") + } +} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt b/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt deleted file mode 100644 index 7dc7c3fc39..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/di/RepositoryBindingsModule.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.ivy.data.di - -import com.ivy.data.repository.AccountRepository -import com.ivy.data.repository.CategoryRepository -import com.ivy.data.repository.CurrencyRepository -import com.ivy.data.repository.ExchangeRatesRepository -import com.ivy.data.repository.LegalRepository -import com.ivy.data.repository.TagRepository -import com.ivy.data.repository.TransactionRepository -import com.ivy.data.repository.impl.AccountRepositoryImpl -import com.ivy.data.repository.impl.CategoryRepositoryImpl -import com.ivy.data.repository.impl.CurrencyRepositoryImpl -import com.ivy.data.repository.impl.ExchangeRatesRepositoryImpl -import com.ivy.data.repository.impl.LegalRepositoryImpl -import com.ivy.data.repository.impl.TagRepositoryImpl -import com.ivy.data.repository.impl.TransactionRepositoryImpl -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -abstract class RepositoryBindingsModule { - @Binds - abstract fun bindAccountRepo(repo: AccountRepositoryImpl): AccountRepository - - @Binds - abstract fun bindCategoryRepo(repo: CategoryRepositoryImpl): CategoryRepository - - @Binds - abstract fun bindTransactionRepo(repo: TransactionRepositoryImpl): TransactionRepository - - @Binds - abstract fun bindExchangeRatesRepo(repo: ExchangeRatesRepositoryImpl): ExchangeRatesRepository - - @Binds - abstract fun bindTagsRepo(repo: TagRepositoryImpl): TagRepository - - @Binds - abstract fun bindCurrencyRepo(repo: CurrencyRepositoryImpl): CurrencyRepository - - @Binds - abstract fun bindLegalRepo(repo: LegalRepositoryImpl): LegalRepository -} diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt index f0edf64986..b5f9ff3fc7 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/AccountRepository.kt @@ -1,15 +1,72 @@ package com.ivy.data.repository +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataWriteEvent +import com.ivy.data.db.dao.read.AccountDao +import com.ivy.data.db.dao.write.WriteAccountDao import com.ivy.data.model.Account import com.ivy.data.model.AccountId +import com.ivy.data.repository.mapper.AccountMapper +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton -interface AccountRepository { - suspend fun findById(id: AccountId): Account? - suspend fun findAll(deleted: Boolean = false): List - suspend fun findMaxOrderNum(): Double +@Singleton +class AccountRepository @Inject constructor( + private val mapper: AccountMapper, + private val accountDao: AccountDao, + private val writeAccountDao: WriteAccountDao, + private val dispatchersProvider: DispatchersProvider, + memoFactory: RepositoryMemoFactory, +) { + private val memo = memoFactory.createMemo( + getDataWriteSaveEvent = DataWriteEvent::SaveAccounts, + getDateWriteDeleteEvent = DataWriteEvent::DeleteAccounts + ) - suspend fun save(value: Account) - suspend fun saveMany(values: List) - suspend fun deleteById(id: AccountId) - suspend fun deleteAll() + suspend fun findById(id: AccountId): Account? = memo.findById( + id = id, + findByIdOperation = { + accountDao.findById(id.value)?.let { + with(mapper) { it.toDomain() }.getOrNull() + } + } + ) + + suspend fun findAll(): List = memo.findAll( + findAllOperation = { + accountDao.findAll().mapNotNull { + with(mapper) { it.toDomain() }.getOrNull() + } + }, + sortMemo = { sortedBy(Account::orderNum) } + ) + + suspend fun findMaxOrderNum(): Double = if (memo.findAllMemoized) { + memo.items.maxOfOrNull { (_, acc) -> acc.orderNum } ?: 0.0 + } else { + withContext(dispatchersProvider.io) { + accountDao.findMaxOrderNum() ?: 0.0 + } + } + + suspend fun save(value: Account): Unit = memo.save(value) { + writeAccountDao.save( + with(mapper) { it.toEntity() } + ) + } + + suspend fun saveMany(values: List): Unit = memo.saveMany(values) { + writeAccountDao.saveMany( + it.map { with(mapper) { it.toEntity() } } + ) + } + + suspend fun deleteById(id: AccountId): Unit = memo.deleteById(id) { + writeAccountDao.deleteById(id.value) + } + + suspend fun deleteAll(): Unit = memo.deleteAll( + deleteAllOperation = writeAccountDao::deleteAll + ) } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt index 3f6b0e3091..ef7735cfe2 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/CategoryRepository.kt @@ -1,15 +1,74 @@ package com.ivy.data.repository +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataWriteEvent +import com.ivy.data.db.dao.read.CategoryDao +import com.ivy.data.db.dao.write.WriteCategoryDao import com.ivy.data.model.Category import com.ivy.data.model.CategoryId +import com.ivy.data.repository.mapper.CategoryMapper +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton -interface CategoryRepository { - suspend fun findAll(deleted: Boolean = false): List - suspend fun findById(id: CategoryId): Category? - suspend fun findMaxOrderNum(): Double +@Singleton +class CategoryRepository @Inject constructor( + private val mapper: CategoryMapper, + private val writeCategoryDao: WriteCategoryDao, + private val categoryDao: CategoryDao, + private val dispatchersProvider: DispatchersProvider, + memoFactory: RepositoryMemoFactory, +) { + private val memo = memoFactory.createMemo( + getDataWriteSaveEvent = DataWriteEvent::SaveCategories, + getDateWriteDeleteEvent = DataWriteEvent::DeleteCategories, + ) - suspend fun save(value: Category) - suspend fun saveMany(values: List) - suspend fun deleteById(id: CategoryId) - suspend fun deleteAll() + suspend fun findAll(): List = memo.findAll( + findAllOperation = { + categoryDao.findAll().mapNotNull { + with(mapper) { it.toDomain() }.getOrNull() + } + }, + sortMemo = { sortedBy(Category::orderNum) } + ) + + suspend fun findById(id: CategoryId): Category? = memo.findById( + id = id, + findByIdOperation = { + categoryDao.findById(id.value)?.let { + with(mapper) { it.toDomain() }.getOrNull() + } + } + ) + + suspend fun findMaxOrderNum(): Double = if (memo.findAllMemoized) { + memo.items.maxOfOrNull { (_, acc) -> acc.orderNum } ?: 0.0 + } else { + withContext(dispatchersProvider.io) { + categoryDao.findMaxOrderNum() ?: 0.0 + } + } + + suspend fun save(value: Category): Unit = memo.save( + value = value, + ) { + writeCategoryDao.save( + with(mapper) { it.toEntity() } + ) + } + + suspend fun saveMany(values: List): Unit = memo.saveMany( + values = values, + ) { + writeCategoryDao.saveMany( + values.map { with(mapper) { it.toEntity() } } + ) + } + + suspend fun deleteById(id: CategoryId): Unit = memo.deleteById(id = id) { + writeCategoryDao.deleteById(id.value) + } + + suspend fun deleteAll(): Unit = memo.deleteAll(writeCategoryDao::deleteAll) } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/CurrencyRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/CurrencyRepository.kt index 611d00fa16..9d4a893c84 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/CurrencyRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/CurrencyRepository.kt @@ -1,8 +1,62 @@ package com.ivy.data.repository +import android.icu.util.Currency +import com.ivy.base.legacy.Theme +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.db.dao.read.SettingsDao +import com.ivy.data.db.dao.write.WriteSettingsDao +import com.ivy.data.db.entity.SettingsEntity import com.ivy.data.model.primitive.AssetCode +import kotlinx.coroutines.withContext +import java.util.Locale +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton -interface CurrencyRepository { - suspend fun getBaseCurrency(): AssetCode - suspend fun setBaseBaseCurrency(newCurrency: AssetCode) +@Singleton +class CurrencyRepository @Inject constructor( + private val settingsDao: SettingsDao, + private val writeSettingsDao: WriteSettingsDao, + private val dispatchersProvider: DispatchersProvider, +) { + companion object { + const val FALLBACK_DEFAULT_CURRENCY = "USD" + } + + private var baseCurrencyMemo: AssetCode? = null + + suspend fun getBaseCurrency(): AssetCode = withContext(dispatchersProvider.io) { + val baseCurrency = baseCurrencyMemo + if (baseCurrency != null) return@withContext baseCurrency + + val currencyCode = settingsDao.findFirstOrNull()?.currency + ?: getDefaultFIATCurrency()?.currencyCode + currencyCode?.let(AssetCode::from)?.getOrNull() + ?: AssetCode.unsafe(FALLBACK_DEFAULT_CURRENCY) + } + + private fun getDefaultFIATCurrency(): Currency? { + return Currency.getInstance(Locale.getDefault()) + } + + suspend fun setBaseBaseCurrency(newCurrency: AssetCode) { + withContext(dispatchersProvider.io) { + val currentEntity = settingsDao.findFirstOrNull() + ?: SettingsEntity( + theme = Theme.AUTO, + currency = FALLBACK_DEFAULT_CURRENCY, + bufferAmount = 0.0, + name = "", + isSynced = true, + isDeleted = false, + id = UUID.randomUUID() + ) + baseCurrencyMemo = newCurrency + writeSettingsDao.save( + currentEntity.copy( + currency = newCurrency.code + ) + ) + } + } } \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/ExchangeRatesRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/ExchangeRatesRepository.kt index 5f88d97254..7a02746bf9 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/ExchangeRatesRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/ExchangeRatesRepository.kt @@ -1,19 +1,78 @@ package com.ivy.data.repository import arrow.core.Either +import arrow.core.raise.either +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.db.dao.read.ExchangeRatesDao +import com.ivy.data.db.dao.write.WriteExchangeRatesDao import com.ivy.data.model.ExchangeRate import com.ivy.data.model.primitive.AssetCode +import com.ivy.data.remote.RemoteExchangeRatesDataSource +import com.ivy.data.repository.mapper.ExchangeRateMapper import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import javax.inject.Inject -interface ExchangeRatesRepository { - suspend fun fetchEurExchangeRates(): Either> +class ExchangeRatesRepository @Inject constructor( + private val mapper: ExchangeRateMapper, + private val exchangeRatesDao: ExchangeRatesDao, + private val writeExchangeRatesDao: WriteExchangeRatesDao, + private val remoteExchangeRatesDataSource: RemoteExchangeRatesDataSource, + private val dispatchers: DispatchersProvider, +) { + suspend fun fetchEurExchangeRates(): Either> = either { + withContext(dispatchers.io) { + val response = remoteExchangeRatesDataSource.fetchEurExchangeRates().bind() + with(mapper) { response.toDomain().bind() } + } + } - fun findAll(): Flow> - suspend fun findAllManuallyOverridden(): List + fun findAll(): Flow> = + exchangeRatesDao.findAll().map { entities -> + entities.mapNotNull { + with(mapper) { it.toDomain().getOrNull() } + } + }.flowOn(dispatchers.io) - suspend fun save(value: ExchangeRate) - suspend fun saveManyRates(values: List) + suspend fun findAllManuallyOverridden(): List = + withContext(dispatchers.io) { + exchangeRatesDao.findAllManuallyOverridden() + .mapNotNull { + with(mapper) { it.toDomain().getOrNull() } + } + } - suspend fun deleteAll() - suspend fun deleteByBaseCurrencyAndCurrency(baseCurrency: AssetCode, currency: AssetCode) + suspend fun save(value: ExchangeRate) { + withContext(dispatchers.io) { + writeExchangeRatesDao.save(with(mapper) { value.toEntity() }) + } + } + + suspend fun saveManyRates(values: List) { + withContext(dispatchers.io) { + writeExchangeRatesDao.saveMany( + values.map { + with(mapper) { it.toEntity() } + }, + ) + } + } + + suspend fun deleteAll() { + withContext(dispatchers.io) { + writeExchangeRatesDao.deleteAll() + } + } + + suspend fun deleteByBaseCurrencyAndCurrency( + baseCurrency: AssetCode, + currency: AssetCode + ): Unit = withContext(dispatchers.io) { + writeExchangeRatesDao.deleteByBaseCurrencyAndCurrency( + baseCurrency = baseCurrency.code, + currency = currency.code + ) + } } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt index 1da7c198fd..8c26aa7943 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/LegalRepository.kt @@ -1,6 +1,21 @@ package com.ivy.data.repository -interface LegalRepository { - suspend fun isDisclaimerAccepted(): Boolean - suspend fun setDisclaimerAccepted(accepted: Boolean) +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.datasource.LocalLegalDataSource +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class LegalRepository @Inject constructor( + private val localLegalDataSource: LocalLegalDataSource, + private val dispatchers: DispatchersProvider +) { + suspend fun isDisclaimerAccepted(): Boolean = withContext(dispatchers.io) { + localLegalDataSource.getIsDisclaimerAccepted() ?: false + } + + suspend fun setDisclaimerAccepted( + accepted: Boolean + ): Unit = withContext(dispatchers.io) { + localLegalDataSource.setDisclaimerAccepted(accepted) + } } \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/RepositoryMemo.kt b/shared/data/core/src/main/java/com/ivy/data/repository/RepositoryMemo.kt index db03b9f4a3..9a14f7d593 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/RepositoryMemo.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/RepositoryMemo.kt @@ -4,7 +4,7 @@ import com.ivy.base.threading.DispatchersProvider import com.ivy.data.DataObserver import com.ivy.data.DataWriteEvent import com.ivy.data.DeleteOperation -import com.ivy.data.model.sync.Syncable +import com.ivy.data.model.sync.Identifiable import com.ivy.data.model.sync.UniqueId import kotlinx.coroutines.withContext import javax.inject.Inject @@ -13,7 +13,7 @@ class RepositoryMemoFactory @Inject constructor( private val dataObserver: DataObserver, private val dispatchers: DispatchersProvider, ) { - fun , TID : UniqueId> createMemo( + fun , TID : UniqueId> createMemo( getDataWriteSaveEvent: (List) -> DataWriteEvent, getDateWriteDeleteEvent: (DeleteOperation) -> DataWriteEvent ): RepositoryMemo = RepositoryMemo( @@ -24,7 +24,7 @@ class RepositoryMemoFactory @Inject constructor( ) } -class RepositoryMemo, TID : UniqueId> internal constructor( +class RepositoryMemo, TID : UniqueId> internal constructor( private val dataObserver: DataObserver, private val dispatchers: DispatchersProvider, private val getDataWriteSaveEvent: (List) -> DataWriteEvent, diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/TagRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/TagRepository.kt index c0adc0f632..a9618c7ee9 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/TagRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/TagRepository.kt @@ -1,22 +1,172 @@ package com.ivy.data.repository +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.DataWriteEvent +import com.ivy.data.db.dao.read.TagAssociationDao +import com.ivy.data.db.dao.read.TagDao +import com.ivy.data.db.dao.write.WriteTagAssociationDao +import com.ivy.data.db.dao.write.WriteTagDao import com.ivy.data.model.Tag import com.ivy.data.model.TagAssociation -import com.ivy.data.model.primitive.AssociationId import com.ivy.data.model.TagId +import com.ivy.data.model.primitive.AssociationId +import com.ivy.data.repository.mapper.TagMapper +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TagRepository @Inject constructor( + private val mapper: TagMapper, + private val tagDao: TagDao, + private val tagAssociationDao: TagAssociationDao, + private val writeTagDao: WriteTagDao, + private val writeTagAssociationDao: WriteTagAssociationDao, + private val dispatchersProvider: DispatchersProvider, + memoFactory: RepositoryMemoFactory, +) { + private val memo = memoFactory.createMemo( + getDataWriteSaveEvent = DataWriteEvent::SaveTags, + getDateWriteDeleteEvent = DataWriteEvent::DeleteTags, + ) + + suspend fun findById(id: TagId): Tag? = memo.findById( + id = id, + findByIdOperation = ::findByIdOperation + ) + + suspend fun findByIds(ids: List): List = memo.findByIds( + ids = ids, + findByIdOperation = ::findByIdOperation, + ) + + private suspend fun findByIdOperation(id: TagId): Tag? = tagDao.findByIds(id.value) + ?.let { + with(mapper) { it.toDomain().getOrNull() } + } + + suspend fun findByAssociatedId(id: AssociationId): List { + return withContext(dispatchersProvider.io) { + tagDao.findTagsByAssociatedId(id.value).let { entities -> + entities.mapNotNull { + with(mapper) { + it.toDomain().getOrNull() + } + } + } + } + } + + suspend fun findByAssociatedId( + ids: List + ): Map> { + return ids.chunked(MAX_SQL_LITE_QUERY_SIZE).map { + withContext(dispatchersProvider.io) { + async { + tagDao.findTagsByAssociatedIds(it.map { it.value }) + .entries.associate { (id, tags) -> + val domainTags = tags.mapNotNull { + with(mapper) { + it.toDomain().getOrNull() + } + } + AssociationId(id) to domainTags + } + } + } + }.awaitAll().asSequence() + .flatMap { it.asSequence() } + .associate { it.key to it.value } + } + + suspend fun findAll(): List = memo.findAll( + findAllOperation = { + tagDao.findAll().let { entities -> + entities.mapNotNull { + with(mapper) { it.toDomain().getOrNull() } + } + } + }, + sortMemo = { + sortedByDescending { it.creationTimestamp.epochSecond } + } + ) + + suspend fun findByText(text: String): List { + return withContext(dispatchersProvider.io) { + tagDao.findByText(text).let { entities -> + entities.mapNotNull { + with(mapper) { it.toDomain().getOrNull() } + } + } + } + } + + suspend fun findByAllAssociatedIdForTagId( + tagIds: List + ): Map> { + return withContext(dispatchersProvider.io) { + tagAssociationDao.findByAllAssociatedIdForTagId( + tagIds.toRawValues() + ).entries.associate { (id, associations) -> + with(mapper) { + TagId(id) to associations.map { it.toDomain() } + } + } + } + } + + suspend fun findByAllTagsForAssociations(): Map> { + return withContext(dispatchersProvider.io) { + tagAssociationDao.findAll().groupBy { + AssociationId(it.associatedId) + }.mapValues { + with(mapper) { + it.value.map { it.toDomain() } + } + } + } + } + + suspend fun associateTagToEntity(associationId: AssociationId, tagId: TagId) { + withContext(dispatchersProvider.io) { + writeTagAssociationDao.save( + with(mapper) { + createNewTagAssociation(tagId, associationId).toEntity() + } + ) + } + } + + suspend fun removeTagAssociation(associationId: AssociationId, tagId: TagId) { + withContext(dispatchersProvider.io) { + writeTagAssociationDao.deleteId(tagId = tagId.value, associatedId = associationId.value) + } + } + + suspend fun save(value: Tag): Unit = memo.save(value) { + writeTagDao.save(with(mapper) { it.toEntity() }) + } + + suspend fun deleteById(id: TagId): Unit = memo.deleteById( + id = id, + deleteByIdOperation = { + writeTagAssociationDao.deleteAssociationsByTagId(it.value) + writeTagDao.deleteById(it.value) + } + ) + + suspend fun deleteAll(): Unit = memo.deleteAll { + writeTagAssociationDao.deleteAll() + writeTagDao.deleteAll() + } + + private fun List.toRawValues(): List = this.map { it.value } -interface TagRepository { - suspend fun findById(id: TagId): Tag? - suspend fun findByIds(ids: List): List - suspend fun findByAssociatedId(id: AssociationId): List - suspend fun findByAssociatedId(ids: List): Map> - suspend fun findAll(deleted: Boolean = false): List - suspend fun findByText(text: String): List - suspend fun findByAllAssociatedIdForTagId(tagIds: List): Map> - suspend fun findByAllTagsForAssociations(): Map> - suspend fun associateTagToEntity(associationId: AssociationId, tagId: TagId) - suspend fun removeTagAssociation(associationId: AssociationId, tagId: TagId) - suspend fun save(value: Tag) - suspend fun deleteById(id: TagId) - suspend fun deleteAll() + companion object { + private const val MAX_SQL_LITE_QUERY_SIZE = 999 + } } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt index 4f993ba294..bba2dfe626 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt @@ -1,112 +1,353 @@ package com.ivy.data.repository import com.ivy.base.model.TransactionType +import com.ivy.base.threading.DispatchersProvider +import com.ivy.data.db.dao.read.TransactionDao +import com.ivy.data.db.dao.write.WriteTransactionDao +import com.ivy.data.db.entity.TransactionEntity import com.ivy.data.model.AccountId import com.ivy.data.model.CategoryId import com.ivy.data.model.Expense import com.ivy.data.model.Income +import com.ivy.data.model.TagId import com.ivy.data.model.Transaction import com.ivy.data.model.TransactionId import com.ivy.data.model.Transfer +import com.ivy.data.model.primitive.AssociationId import com.ivy.data.model.primitive.NonNegativeLong +import com.ivy.data.model.primitive.toNonNegative +import com.ivy.data.repository.mapper.TransactionMapper +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext import java.time.LocalDateTime import java.util.UUID +import javax.inject.Inject -interface TransactionRepository { - suspend fun findById(id: TransactionId): Transaction? - suspend fun findByIds(ids: List): List +class TransactionRepository @Inject constructor( + private val mapper: TransactionMapper, + private val transactionDao: TransactionDao, + private val writeTransactionDao: WriteTransactionDao, + private val dispatchersProvider: DispatchersProvider, + private val tagRepository: TagRepository +) { + suspend fun findAll(): List = withContext(dispatchersProvider.io) { + val tagMap = async { findAllTagAssociations() } + retrieveTrns( + dbCall = transactionDao::findAll, + retrieveTags = { + tagMap.await()[it.id] ?: emptyList() + } + ) + } - suspend fun findAll(): List - suspend fun findAllIncomeByAccount(accountId: AccountId): List - suspend fun findAllExpenseByAccount(accountId: AccountId): List - suspend fun findAllTransferByAccount(accountId: AccountId): List - suspend fun findAllTransfersToAccount(toAccountId: AccountId): List + suspend fun findAllIncomeByAccount( + accountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByTypeAndAccount( + type = TransactionType.INCOME, + accountId = accountId.value + ) + } + ).filterIsInstance() + + suspend fun findAllExpenseByAccount( + accountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByTypeAndAccount( + type = TransactionType.EXPENSE, + accountId = accountId.value + ) + } + ).filterIsInstance() + + suspend fun findAllTransferByAccount( + accountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByTypeAndAccount( + type = TransactionType.TRANSFER, + accountId = accountId.value + ) + } + ).filterIsInstance() + + suspend fun findAllTransfersToAccount( + toAccountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllTransfersToAccount(toAccountId = toAccountId.value) + } + ).filterIsInstance() suspend fun findAllBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = withContext(dispatchersProvider.io) { + val transactions = transactionDao.findAllBetween(startDate, endDate) + val tagAssociationMap = getTagsForTransactionIds(transactions) + transactions.mapNotNull { + val tags = tagAssociationMap[it.id] ?: emptyList() + with(mapper) { it.toDomain(tags = tags) }.getOrNull() + } + } suspend fun findAllByAccountAndBetween( accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByAccountAndBetween( + accountId = accountId.value, + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllToAccountAndBetween( toAccountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllToAccountAndBetween( + toAccountId = toAccountId.value, + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllDueToBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetween( + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllDueToBetweenByCategory( startDate: LocalDateTime, endDate: LocalDateTime, categoryId: CategoryId - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetweenByCategory( + startDate = startDate, + endDate = endDate, + categoryId = categoryId.value + ) + } + ) suspend fun findAllDueToBetweenByCategoryUnspecified( startDate: LocalDateTime, - endDate: LocalDateTime, - ): List + endDate: LocalDateTime + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetweenByCategoryUnspecified( + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllDueToBetweenByAccount( startDate: LocalDateTime, endDate: LocalDateTime, accountId: AccountId - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetweenByAccount( + startDate = startDate, + endDate = endDate, + accountId = accountId.value + ) + } + ) suspend fun findAllByCategoryAndTypeAndBetween( categoryId: UUID, type: TransactionType, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByCategoryAndTypeAndBetween( + categoryId = categoryId, + type = type, + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllUnspecifiedAndTypeAndBetween( type: TransactionType, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllUnspecifiedAndTypeAndBetween( + type = type, + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllUnspecifiedAndBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllUnspecifiedAndBetween( + startDate = startDate, + endDate = endDate + ) + } + ) suspend fun findAllByCategoryAndBetween( categoryId: UUID, startDate: LocalDateTime, endDate: LocalDateTime - ): List + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByCategoryAndBetween( + categoryId = categoryId, + startDate = startDate, + endDate = endDate + ) + } + ) + + suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List = retrieveTrns( + dbCall = { + transactionDao.findAllByRecurringRuleId(recurringRuleId) + } + ) + + suspend fun findById( + id: TransactionId + ): Transaction? = withContext(dispatchersProvider.io) { + transactionDao.findById(id.value)?.let { + with(mapper) { it.toDomain() }.getOrNull() + } + } + + suspend fun findByIds(ids: List): List { + return withContext(dispatchersProvider.io) { + val tagMap = async { findTagsForTransactionIds(ids) } + retrieveTrns( + dbCall = { + transactionDao.findByIds(ids.map { it.value }) + }, + retrieveTags = { + tagMap.await()[it.id] ?: emptyList() + } + ) + } + } + + suspend fun save(value: Transaction) { + withContext(dispatchersProvider.io) { + writeTransactionDao.save( + with(mapper) { value.toEntity() } + ) + } + } + + suspend fun saveMany(value: List) { + withContext(dispatchersProvider.io) { + writeTransactionDao.saveMany( + value.map { with(mapper) { it.toEntity() } } + ) + } + } + + suspend fun deleteById(id: TransactionId) { + withContext(dispatchersProvider.io) { + writeTransactionDao.deleteById(id.value) + } + } + + suspend fun deleteAllByAccountId(accountId: AccountId) { + withContext(dispatchersProvider.io) { + writeTransactionDao.deleteAllByAccountId(accountId.value) + } + } + + suspend fun deletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) { + withContext(dispatchersProvider.io) { + writeTransactionDao.deletedByRecurringRuleIdAndNoDateTime(recurringRuleId) + } + } + + suspend fun deleteAll() { + withContext(dispatchersProvider.io) { + writeTransactionDao.deleteAll() + } + } + + suspend fun countHappenedTransactions(): NonNegativeLong = withContext(dispatchersProvider.io) { + transactionDao.countHappenedTransactions().toNonNegative() + } + + suspend fun findLoanTransaction(loanId: UUID): Transaction? = + withContext(dispatchersProvider.io) { + transactionDao.findLoanTransaction(loanId)?.let { + with(mapper) { it.toDomain() }.getOrNull() + } + } - suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List - suspend fun flagDeletedByAccountId(accountId: UUID) + suspend fun findLoanRecordTransaction(loanRecordId: UUID): Transaction? = + withContext(dispatchersProvider.io) { + transactionDao.findLoanRecordTransaction(loanRecordId)?.let { + with(mapper) { it.toDomain() }.getOrNull() + } + } - suspend fun save(value: Transaction) - suspend fun saveMany(value: List) + suspend fun findAllByLoanId(loanId: UUID): List = retrieveTrns( + dbCall = { + transactionDao.findAllByLoanId(loanId) + } + ) - suspend fun flagDeleted(id: TransactionId) - suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) - suspend fun deleteById(id: TransactionId) - suspend fun deleteAllByAccountId(accountId: AccountId) - suspend fun deleteAll() + private suspend fun retrieveTrns( + dbCall: suspend () -> List, + retrieveTags: suspend (TransactionEntity) -> List = { emptyList() }, + ): List = withContext(dispatchersProvider.io) { + dbCall().mapNotNull { + with(mapper) { it.toDomain(tags = retrieveTags(it)) }.getOrNull() + } + } - suspend fun countHappenedTransactions(): NonNegativeLong - suspend fun findLoanTransaction( - loanId: UUID - ): Transaction? + private suspend fun getTagsForTransactionIds( + transactions: List + ): Map> { + return findTagsForTransactionIds(transactions.map { TransactionId(it.id) }) + } - suspend fun findLoanRecordTransaction( - loanRecordId: UUID - ): Transaction? + private suspend fun findTagsForTransactionIds( + transactionIds: List + ): Map> { + return tagRepository.findByAssociatedId(transactionIds.map { AssociationId(it.value) }) + .entries.associate { + it.key.value to it.value.map { ta -> ta.id } + } + } - suspend fun findAllByLoanId( - loanId: UUID - ): List + private suspend fun findAllTagAssociations(): Map> { + return tagRepository.findByAllTagsForAssociations().entries.associate { + it.key.value to it.value.map { ta -> ta.id } + } + } } \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt deleted file mode 100644 index 9d9aeacd4c..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeAccountRepository.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.ivy.data.repository.fake - -import com.ivy.base.TestDispatchersProvider -import com.ivy.data.db.dao.read.AccountDao -import com.ivy.data.db.dao.read.SettingsDao -import com.ivy.data.db.dao.write.WriteAccountDao -import com.ivy.data.db.dao.write.WriteSettingsDao -import com.ivy.data.repository.AccountRepository -import com.ivy.data.repository.impl.AccountRepositoryImpl -import com.ivy.data.repository.mapper.AccountMapper -import org.jetbrains.annotations.VisibleForTesting - -@VisibleForTesting -class FakeAccountRepository( - accountDao: AccountDao, - writeAccountDao: WriteAccountDao, - settingsDao: SettingsDao, - writeSettingsDao: WriteSettingsDao, - private val accountRepository: AccountRepository = AccountRepositoryImpl( - mapper = AccountMapper( - FakeCurrencyRepository( - settingsDao = settingsDao, - writeSettingsDao = writeSettingsDao - ) - ), - accountDao = accountDao, - writeAccountDao = writeAccountDao, - dispatchersProvider = TestDispatchersProvider, - memoFactory = fakeRepositoryMakeFactory() - ) -) : AccountRepository by accountRepository \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeCurrencyRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeCurrencyRepository.kt deleted file mode 100644 index 0203d3a00f..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeCurrencyRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.ivy.data.repository.fake - -import com.ivy.data.db.dao.read.SettingsDao -import com.ivy.data.db.dao.write.WriteSettingsDao -import com.ivy.data.repository.CurrencyRepository -import com.ivy.data.repository.impl.CurrencyRepositoryImpl -import com.ivy.base.TestDispatchersProvider -import org.jetbrains.annotations.VisibleForTesting - -@VisibleForTesting -class FakeCurrencyRepository( - settingsDao: SettingsDao, - writeSettingsDao: WriteSettingsDao, - private val currencyRepository: CurrencyRepository = CurrencyRepositoryImpl( - settingsDao = settingsDao, - writeSettingsDao = writeSettingsDao, - dispatchersProvider = TestDispatchersProvider, - ) -) : CurrencyRepository by currencyRepository \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeRepositoryMemo.kt b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeRepositoryMemo.kt index 95a74b8fc0..1c98e365b4 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeRepositoryMemo.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeRepositoryMemo.kt @@ -6,7 +6,7 @@ import com.ivy.data.repository.RepositoryMemoFactory import org.jetbrains.annotations.VisibleForTesting @VisibleForTesting -fun fakeRepositoryMakeFactory(): RepositoryMemoFactory = RepositoryMemoFactory( +fun fakeRepositoryMemoFactory(): RepositoryMemoFactory = RepositoryMemoFactory( dataObserver = DataObserver(), dispatchers = TestDispatchersProvider ) \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt deleted file mode 100644 index cd118bd3af..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/fake/FakeTagRepository.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.ivy.data.repository.fake - -import com.ivy.base.TestDispatchersProvider -import com.ivy.data.db.dao.read.TagAssociationDao -import com.ivy.data.db.dao.read.TagDao -import com.ivy.data.db.dao.write.WriteTagAssociationDao -import com.ivy.data.db.dao.write.WriteTagDao -import com.ivy.data.repository.TagRepository -import com.ivy.data.repository.impl.TagRepositoryImpl -import com.ivy.data.repository.mapper.TagMapper -import org.jetbrains.annotations.VisibleForTesting - -@VisibleForTesting -class FakeTagRepository( - tagDao: TagDao, - tagAssociationDao: TagAssociationDao, - writeTagDao: WriteTagDao, - writeTagAssociationDao: WriteTagAssociationDao, - private val tagRepository: TagRepository = TagRepositoryImpl( - mapper = TagMapper(), - tagDao = tagDao, - tagAssociationDao = tagAssociationDao, - writeTagDao = writeTagDao, - writeTagAssociationDao = writeTagAssociationDao, - dispatchersProvider = TestDispatchersProvider, - memoFactory = fakeRepositoryMakeFactory(), - ) -) : TagRepository by tagRepository \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt deleted file mode 100644 index 619948c40a..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/AccountRepositoryImpl.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.ivy.data.repository.impl - -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.DataWriteEvent -import com.ivy.data.db.dao.read.AccountDao -import com.ivy.data.db.dao.write.WriteAccountDao -import com.ivy.data.model.Account -import com.ivy.data.model.AccountId -import com.ivy.data.repository.AccountRepository -import com.ivy.data.repository.RepositoryMemoFactory -import com.ivy.data.repository.mapper.AccountMapper -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AccountRepositoryImpl @Inject constructor( - private val mapper: AccountMapper, - private val accountDao: AccountDao, - private val writeAccountDao: WriteAccountDao, - private val dispatchersProvider: DispatchersProvider, - memoFactory: RepositoryMemoFactory, -) : AccountRepository { - - private val memo = memoFactory.createMemo( - getDataWriteSaveEvent = DataWriteEvent::SaveAccounts, - getDateWriteDeleteEvent = DataWriteEvent::DeleteAccounts - ) - - override suspend fun findById(id: AccountId): Account? = memo.findById( - id = id, - findByIdOperation = { - accountDao.findById(id.value)?.let { - with(mapper) { it.toDomain() }.getOrNull() - } - } - ) - - override suspend fun findAll(deleted: Boolean): List = memo.findAll( - findAllOperation = { - accountDao.findAll(deleted).mapNotNull { - with(mapper) { it.toDomain() }.getOrNull() - } - }, - sortMemo = { sortedBy(Account::orderNum) } - ) - - override suspend fun findMaxOrderNum(): Double = if (memo.findAllMemoized) { - memo.items.maxOfOrNull { (_, acc) -> acc.orderNum } ?: 0.0 - } else { - withContext(dispatchersProvider.io) { - accountDao.findMaxOrderNum() ?: 0.0 - } - } - - override suspend fun save(value: Account): Unit = memo.save(value) { - writeAccountDao.save( - with(mapper) { it.toEntity() } - ) - } - - override suspend fun saveMany(values: List): Unit = memo.saveMany(values) { - writeAccountDao.saveMany( - it.map { with(mapper) { it.toEntity() } } - ) - } - - override suspend fun deleteById(id: AccountId): Unit = memo.deleteById(id) { - writeAccountDao.deleteById(id.value) - } - - override suspend fun deleteAll(): Unit = memo.deleteAll( - deleteAllOperation = writeAccountDao::deleteAll - ) -} diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt deleted file mode 100644 index 8fa7476934..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/CategoryRepositoryImpl.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.ivy.data.repository.impl - -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.DataWriteEvent -import com.ivy.data.db.dao.read.CategoryDao -import com.ivy.data.db.dao.write.WriteCategoryDao -import com.ivy.data.model.Category -import com.ivy.data.model.CategoryId -import com.ivy.data.repository.CategoryRepository -import com.ivy.data.repository.RepositoryMemoFactory -import com.ivy.data.repository.mapper.CategoryMapper -import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CategoryRepositoryImpl @Inject constructor( - private val mapper: CategoryMapper, - private val writeCategoryDao: WriteCategoryDao, - private val categoryDao: CategoryDao, - private val dispatchersProvider: DispatchersProvider, - memoFactory: RepositoryMemoFactory, -) : CategoryRepository { - - private val memo = memoFactory.createMemo( - getDataWriteSaveEvent = DataWriteEvent::SaveCategories, - getDateWriteDeleteEvent = DataWriteEvent::DeleteCategories, - ) - - override suspend fun findAll(deleted: Boolean): List = memo.findAll( - findAllOperation = { - categoryDao.findAll(deleted).mapNotNull { - with(mapper) { it.toDomain() }.getOrNull() - } - }, - sortMemo = { sortedBy(Category::orderNum) } - ) - - override suspend fun findById(id: CategoryId): Category? = memo.findById( - id = id, - findByIdOperation = { - categoryDao.findById(id.value)?.let { - with(mapper) { it.toDomain() }.getOrNull() - } - } - ) - - override suspend fun findMaxOrderNum(): Double = if (memo.findAllMemoized) { - memo.items.maxOfOrNull { (_, acc) -> acc.orderNum } ?: 0.0 - } else { - withContext(dispatchersProvider.io) { - categoryDao.findMaxOrderNum() ?: 0.0 - } - } - - override suspend fun save(value: Category): Unit = memo.save( - value = value, - ) { - writeCategoryDao.save( - with(mapper) { it.toEntity() } - ) - } - - override suspend fun saveMany(values: List): Unit = memo.saveMany( - values = values, - ) { - writeCategoryDao.saveMany( - values.map { with(mapper) { it.toEntity() } } - ) - } - - override suspend fun deleteById(id: CategoryId): Unit = memo.deleteById(id = id) { - writeCategoryDao.deleteById(id.value) - } - - override suspend fun deleteAll(): Unit = memo.deleteAll(writeCategoryDao::deleteAll) -} diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/CurrencyRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/CurrencyRepositoryImpl.kt deleted file mode 100644 index 7482896f48..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/CurrencyRepositoryImpl.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.ivy.data.repository.impl - -import android.icu.util.Currency -import com.ivy.base.legacy.Theme -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.db.dao.read.SettingsDao -import com.ivy.data.db.dao.write.WriteSettingsDao -import com.ivy.data.db.entity.SettingsEntity -import com.ivy.data.model.primitive.AssetCode -import com.ivy.data.repository.CurrencyRepository -import kotlinx.coroutines.withContext -import java.util.Locale -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CurrencyRepositoryImpl @Inject constructor( - private val settingsDao: SettingsDao, - private val writeSettingsDao: WriteSettingsDao, - private val dispatchersProvider: DispatchersProvider, -) : CurrencyRepository { - companion object { - const val FALLBACK_DEFAULT_CURRENCY = "USD" - } - - private var baseCurrencyMemo: AssetCode? = null - - override suspend fun getBaseCurrency(): AssetCode = withContext(dispatchersProvider.io) { - val baseCurrency = baseCurrencyMemo - if (baseCurrency != null) return@withContext baseCurrency - - val currencyCode = settingsDao.findFirstOrNull()?.currency - ?: getDefaultFIATCurrency()?.currencyCode - currencyCode?.let(AssetCode::from)?.getOrNull() - ?: AssetCode.unsafe(FALLBACK_DEFAULT_CURRENCY) - } - - private fun getDefaultFIATCurrency(): Currency? { - return Currency.getInstance(Locale.getDefault()) - } - - override suspend fun setBaseBaseCurrency(newCurrency: AssetCode) { - withContext(dispatchersProvider.io) { - val currentEntity = settingsDao.findFirstOrNull() - ?: SettingsEntity( - theme = Theme.AUTO, - currency = FALLBACK_DEFAULT_CURRENCY, - bufferAmount = 0.0, - name = "", - isSynced = true, - isDeleted = false, - id = UUID.randomUUID() - ) - baseCurrencyMemo = newCurrency - writeSettingsDao.save( - currentEntity.copy( - currency = newCurrency.code - ) - ) - } - } -} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImpl.kt deleted file mode 100644 index 23fbdfdf9e..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImpl.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.ivy.data.repository.impl - -import arrow.core.Either -import arrow.core.raise.either -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.db.dao.read.ExchangeRatesDao -import com.ivy.data.db.dao.write.WriteExchangeRatesDao -import com.ivy.data.model.ExchangeRate -import com.ivy.data.model.primitive.AssetCode -import com.ivy.data.remote.RemoteExchangeRatesDataSource -import com.ivy.data.repository.ExchangeRatesRepository -import com.ivy.data.repository.mapper.ExchangeRateMapper -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import javax.inject.Inject - -class ExchangeRatesRepositoryImpl @Inject constructor( - private val mapper: ExchangeRateMapper, - private val exchangeRatesDao: ExchangeRatesDao, - private val writeExchangeRatesDao: WriteExchangeRatesDao, - private val remoteExchangeRatesDataSource: RemoteExchangeRatesDataSource, - private val dispatchers: DispatchersProvider, -) : ExchangeRatesRepository { - override suspend fun fetchEurExchangeRates(): Either> = either { - withContext(dispatchers.io) { - val response = remoteExchangeRatesDataSource.fetchEurExchangeRates().bind() - with(mapper) { response.toDomain().bind() } - } - } - - override fun findAll(): Flow> = - exchangeRatesDao.findAll().map { entities -> - entities.mapNotNull { - with(mapper) { it.toDomain().getOrNull() } - } - }.flowOn(dispatchers.io) - - override suspend fun findAllManuallyOverridden(): List = - withContext(dispatchers.io) { - exchangeRatesDao.findAllManuallyOverridden() - .mapNotNull { - with(mapper) { it.toDomain().getOrNull() } - } - } - - override suspend fun save(value: ExchangeRate) { - withContext(dispatchers.io) { - writeExchangeRatesDao.save(with(mapper) { value.toEntity() }) - } - } - - override suspend fun saveManyRates(values: List) { - withContext(dispatchers.io) { - writeExchangeRatesDao.saveMany( - values.map { - with(mapper) { it.toEntity() } - }, - ) - } - } - - override suspend fun deleteAll() { - withContext(dispatchers.io) { - writeExchangeRatesDao.deleteAll() - } - } - - override suspend fun deleteByBaseCurrencyAndCurrency( - baseCurrency: AssetCode, - currency: AssetCode - ): Unit = withContext(dispatchers.io) { - writeExchangeRatesDao.deleteByBaseCurrencyAndCurrency( - baseCurrency = baseCurrency.code, - currency = currency.code - ) - } -} diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt deleted file mode 100644 index 3d01ea17ad..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/LegalRepositoryImpl.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.ivy.data.repository.impl - -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.datasource.LocalLegalDataSource -import com.ivy.data.repository.LegalRepository -import kotlinx.coroutines.withContext -import javax.inject.Inject - -class LegalRepositoryImpl @Inject constructor( - private val localLegalDataSource: LocalLegalDataSource, - private val dispatchers: DispatchersProvider -) : LegalRepository { - override suspend fun isDisclaimerAccepted(): Boolean = withContext(dispatchers.io) { - localLegalDataSource.getIsDisclaimerAccepted() ?: false - } - - override suspend fun setDisclaimerAccepted( - accepted: Boolean - ): Unit = withContext(dispatchers.io) { - localLegalDataSource.setDisclaimerAccepted(accepted) - } -} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagRepositoryImpl.kt deleted file mode 100644 index 9b815ea1d2..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TagRepositoryImpl.kt +++ /dev/null @@ -1,175 +0,0 @@ -package com.ivy.data.repository.impl - -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.DataWriteEvent -import com.ivy.data.db.dao.read.TagAssociationDao -import com.ivy.data.db.dao.read.TagDao -import com.ivy.data.db.dao.write.WriteTagAssociationDao -import com.ivy.data.db.dao.write.WriteTagDao -import com.ivy.data.model.Tag -import com.ivy.data.model.TagAssociation -import com.ivy.data.model.TagId -import com.ivy.data.model.primitive.AssociationId -import com.ivy.data.repository.RepositoryMemoFactory -import com.ivy.data.repository.TagRepository -import com.ivy.data.repository.mapper.TagMapper -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.withContext -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class TagRepositoryImpl @Inject constructor( - private val mapper: TagMapper, - private val tagDao: TagDao, - private val tagAssociationDao: TagAssociationDao, - private val writeTagDao: WriteTagDao, - private val writeTagAssociationDao: WriteTagAssociationDao, - private val dispatchersProvider: DispatchersProvider, - memoFactory: RepositoryMemoFactory, -) : TagRepository { - - private val memo = memoFactory.createMemo( - getDataWriteSaveEvent = DataWriteEvent::SaveTags, - getDateWriteDeleteEvent = DataWriteEvent::DeleteTags, - ) - - override suspend fun findById(id: TagId): Tag? = memo.findById( - id = id, - findByIdOperation = ::findByIdOperation - ) - - override suspend fun findByIds(ids: List): List = memo.findByIds( - ids = ids, - findByIdOperation = ::findByIdOperation, - ) - - private suspend fun findByIdOperation(id: TagId): Tag? = tagDao.findByIds(id.value) - ?.let { - with(mapper) { it.toDomain().getOrNull() } - } - - override suspend fun findByAssociatedId(id: AssociationId): List { - return withContext(dispatchersProvider.io) { - tagDao.findTagsByAssociatedId(id.value).let { entities -> - entities.mapNotNull { - with(mapper) { - it.toDomain().getOrNull() - } - } - } - } - } - - override suspend fun findByAssociatedId( - ids: List - ): Map> { - return ids.chunked(MAX_SQL_LITE_QUERY_SIZE).map { - withContext(dispatchersProvider.io) { - async { - tagDao.findTagsByAssociatedIds(it.map { it.value }) - .entries.associate { (id, tags) -> - val domainTags = tags.mapNotNull { - with(mapper) { - it.toDomain().getOrNull() - } - } - AssociationId(id) to domainTags - } - } - } - }.awaitAll().asSequence() - .flatMap { it.asSequence() } - .associate { it.key to it.value } - } - - override suspend fun findAll(deleted: Boolean): List = memo.findAll( - findAllOperation = { - tagDao.findAll().let { entities -> - entities.mapNotNull { - with(mapper) { it.toDomain().getOrNull() } - } - } - }, - sortMemo = { - sortedByDescending { it.creationTimestamp.epochSecond } - } - ) - - override suspend fun findByText(text: String): List { - return withContext(dispatchersProvider.io) { - tagDao.findByText(text).let { entities -> - entities.mapNotNull { - with(mapper) { it.toDomain().getOrNull() } - } - } - } - } - - override suspend fun findByAllAssociatedIdForTagId( - tagIds: List - ): Map> { - return withContext(dispatchersProvider.io) { - tagAssociationDao.findByAllAssociatedIdForTagId( - tagIds.toRawValues() - ).entries.associate { (id, associations) -> - with(mapper) { - TagId(id) to associations.map { it.toDomain() } - } - } - } - } - - override suspend fun findByAllTagsForAssociations(): Map> { - return withContext(dispatchersProvider.io) { - tagAssociationDao.findAll().groupBy { - AssociationId(it.associatedId) - }.mapValues { - with(mapper) { - it.value.map { it.toDomain() } - } - } - } - } - - override suspend fun associateTagToEntity(associationId: AssociationId, tagId: TagId) { - withContext(dispatchersProvider.io) { - writeTagAssociationDao.save( - with(mapper) { - createNewTagAssociation(tagId, associationId).toEntity() - } - ) - } - } - - override suspend fun removeTagAssociation(associationId: AssociationId, tagId: TagId) { - withContext(dispatchersProvider.io) { - writeTagAssociationDao.deleteId(tagId = tagId.value, associatedId = associationId.value) - } - } - - override suspend fun save(value: Tag): Unit = memo.save(value) { - writeTagDao.save(with(mapper) { it.toEntity() }) - } - - override suspend fun deleteById(id: TagId): Unit = memo.deleteById( - id = id, - deleteByIdOperation = { - writeTagAssociationDao.deleteAssociationsByTagId(it.value) - writeTagDao.deleteById(it.value) - } - ) - - override suspend fun deleteAll(): Unit = memo.deleteAll { - writeTagAssociationDao.deleteAll() - writeTagDao.deleteAll() - } - - private fun List.toRawValues(): List = this.map { it.value } - - companion object { - private const val MAX_SQL_LITE_QUERY_SIZE = 999 - } -} diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt deleted file mode 100644 index 3be372558f..0000000000 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt +++ /dev/null @@ -1,367 +0,0 @@ -package com.ivy.data.repository.impl - -import com.ivy.base.model.TransactionType -import com.ivy.base.threading.DispatchersProvider -import com.ivy.data.db.dao.read.TransactionDao -import com.ivy.data.db.dao.write.WriteTransactionDao -import com.ivy.data.db.entity.TransactionEntity -import com.ivy.data.model.AccountId -import com.ivy.data.model.CategoryId -import com.ivy.data.model.Expense -import com.ivy.data.model.Income -import com.ivy.data.model.TagId -import com.ivy.data.model.Transaction -import com.ivy.data.model.TransactionId -import com.ivy.data.model.Transfer -import com.ivy.data.model.primitive.AssociationId -import com.ivy.data.model.primitive.NonNegativeLong -import com.ivy.data.model.primitive.toNonNegative -import com.ivy.data.repository.TagRepository -import com.ivy.data.repository.TransactionRepository -import com.ivy.data.repository.mapper.TransactionMapper -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext -import java.time.LocalDateTime -import java.util.UUID -import javax.inject.Inject - -class TransactionRepositoryImpl @Inject constructor( - private val mapper: TransactionMapper, - private val transactionDao: TransactionDao, - private val writeTransactionDao: WriteTransactionDao, - private val dispatchersProvider: DispatchersProvider, - private val tagRepository: TagRepository -) : TransactionRepository { - override suspend fun findAll(): List = withContext(dispatchersProvider.io) { - val tagMap = async { findAllTagAssociations() } - retrieveTrns( - dbCall = transactionDao::findAll, - retrieveTags = { - tagMap.await()[it.id] ?: emptyList() - } - ) - } - - override suspend fun findAllIncomeByAccount( - accountId: AccountId - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllByTypeAndAccount( - type = TransactionType.INCOME, - accountId = accountId.value - ) - } - ).filterIsInstance() - - override suspend fun findAllExpenseByAccount( - accountId: AccountId - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllByTypeAndAccount( - type = TransactionType.EXPENSE, - accountId = accountId.value - ) - } - ).filterIsInstance() - - override suspend fun findAllTransferByAccount( - accountId: AccountId - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllByTypeAndAccount( - type = TransactionType.TRANSFER, - accountId = accountId.value - ) - } - ).filterIsInstance() - - override suspend fun findAllTransfersToAccount( - toAccountId: AccountId - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllTransfersToAccount(toAccountId = toAccountId.value) - } - ).filterIsInstance() - - override suspend fun findAllBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = withContext(dispatchersProvider.io) { - val transactions = transactionDao.findAllBetween(startDate, endDate) - val tagAssociationMap = getTagsForTransactionIds(transactions) - transactions.mapNotNull { - val tags = tagAssociationMap[it.id] ?: emptyList() - with(mapper) { it.toDomain(tags = tags) }.getOrNull() - } - } - - override suspend fun findAllByAccountAndBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllByAccountAndBetween( - accountId = accountId.value, - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllToAccountAndBetween( - toAccountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllToAccountAndBetween( - toAccountId = toAccountId.value, - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllDueToBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllDueToBetween( - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllDueToBetweenByCategory( - startDate: LocalDateTime, - endDate: LocalDateTime, - categoryId: CategoryId - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllDueToBetweenByCategory( - startDate = startDate, - endDate = endDate, - categoryId = categoryId.value - ) - } - ) - - override suspend fun findAllDueToBetweenByCategoryUnspecified( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllDueToBetweenByCategoryUnspecified( - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllDueToBetweenByAccount( - startDate: LocalDateTime, - endDate: LocalDateTime, - accountId: AccountId - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllDueToBetweenByAccount( - startDate = startDate, - endDate = endDate, - accountId = accountId.value - ) - } - ) - - override suspend fun findAllByCategoryAndTypeAndBetween( - categoryId: UUID, - type: TransactionType, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllByCategoryAndTypeAndBetween( - categoryId = categoryId, - type = type, - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllUnspecifiedAndTypeAndBetween( - type: TransactionType, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllUnspecifiedAndTypeAndBetween( - type = type, - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllUnspecifiedAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllUnspecifiedAndBetween( - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllByCategoryAndBetween( - categoryId: UUID, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List = retrieveTrns( - dbCall = { - transactionDao.findAllByCategoryAndBetween( - categoryId = categoryId, - startDate = startDate, - endDate = endDate - ) - } - ) - - override suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List = retrieveTrns( - dbCall = { - transactionDao.findAllByRecurringRuleId(recurringRuleId) - } - ) - - override suspend fun flagDeletedByAccountId(accountId: UUID) { - withContext(dispatchersProvider.io) { - writeTransactionDao.flagDeletedByAccountId(accountId) - } - } - - override suspend fun findById( - id: TransactionId - ): Transaction? = withContext(dispatchersProvider.io) { - transactionDao.findById(id.value)?.let { - with(mapper) { it.toDomain() }.getOrNull() - } - } - - override suspend fun findByIds(ids: List): List { - return withContext(dispatchersProvider.io) { - val tagMap = async { findTagsForTransactionIds(ids) } - retrieveTrns( - dbCall = { - transactionDao.findByIds(ids.map { it.value }) - }, - retrieveTags = { - tagMap.await()[it.id] ?: emptyList() - } - ) - } - } - - override suspend fun save(value: Transaction) { - withContext(dispatchersProvider.io) { - writeTransactionDao.save( - with(mapper) { value.toEntity() } - ) - } - } - - override suspend fun saveMany(value: List) { - withContext(dispatchersProvider.io) { - writeTransactionDao.saveMany( - value.map { with(mapper) { it.toEntity() } } - ) - } - } - - override suspend fun flagDeleted(id: TransactionId) { - withContext(dispatchersProvider.io) { - writeTransactionDao.flagDeleted(id.value) - } - } - - override suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) { - withContext(dispatchersProvider.io) { - writeTransactionDao.flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId) - } - } - - override suspend fun deleteById(id: TransactionId) { - withContext(dispatchersProvider.io) { - writeTransactionDao.deleteById(id.value) - } - } - - override suspend fun deleteAllByAccountId(accountId: AccountId) { - withContext(dispatchersProvider.io) { - writeTransactionDao.deleteAllByAccountId(accountId.value) - } - } - - override suspend fun deleteAll() { - withContext(dispatchersProvider.io) { - writeTransactionDao.deleteAll() - } - } - - override suspend fun countHappenedTransactions(): NonNegativeLong = withContext(dispatchersProvider.io) { - transactionDao.countHappenedTransactions().toNonNegative() - } - - override suspend fun findLoanTransaction(loanId: UUID): Transaction? = - withContext(dispatchersProvider.io) { - transactionDao.findLoanTransaction(loanId)?.let { - with(mapper) { it.toDomain() }.getOrNull() - } - } - - override suspend fun findLoanRecordTransaction(loanRecordId: UUID): Transaction? = - withContext(dispatchersProvider.io) { - transactionDao.findLoanRecordTransaction(loanRecordId)?.let { - with(mapper) { it.toDomain() }.getOrNull() - } - } - - override suspend fun findAllByLoanId(loanId: UUID): List = retrieveTrns( - dbCall = { - transactionDao.findAllByLoanId(loanId) - } - ) - - private suspend fun retrieveTrns( - dbCall: suspend () -> List, - retrieveTags: suspend (TransactionEntity) -> List = { emptyList() }, - ): List = withContext(dispatchersProvider.io) { - dbCall().mapNotNull { - with(mapper) { it.toDomain(tags = retrieveTags(it)) }.getOrNull() - } - } - - private suspend fun getTagsForTransactionIds( - transactions: List - ): Map> { - return findTagsForTransactionIds(transactions.map { TransactionId(it.id) }) - } - - private suspend fun findTagsForTransactionIds( - transactionIds: List - ): Map> { - return tagRepository.findByAssociatedId(transactionIds.map { AssociationId(it.value) }) - .entries.associate { - it.key.value to it.value.map { ta -> ta.id } - } - } - - private suspend fun findAllTagAssociations(): Map> { - return tagRepository.findByAllTagsForAssociations().entries.associate { - it.key.value to it.value.map { ta -> ta.id } - } - } -} \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt index 1502db09c8..1509f9759e 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt @@ -2,6 +2,7 @@ package com.ivy.data.repository.mapper import arrow.core.Either import arrow.core.raise.either +import arrow.core.raise.ensure import com.ivy.data.db.entity.AccountEntity import com.ivy.data.model.Account import com.ivy.data.model.AccountId @@ -10,13 +11,14 @@ import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.data.repository.CurrencyRepository -import java.time.Instant import javax.inject.Inject class AccountMapper @Inject constructor( private val currencyRepository: CurrencyRepository ) { suspend fun AccountEntity.toDomain(): Either = either { + ensure(!isDeleted) { "Account is deleted" } + Account( id = AccountId(id), name = NotBlankTrimmedString.from(name).bind(), @@ -26,8 +28,6 @@ class AccountMapper @Inject constructor( icon = icon?.let(IconAsset::from)?.getOrNull(), includeInBalance = includeInBalance, orderNum = orderNum, - lastUpdated = Instant.EPOCH, // TODO: Wire this - removed = isDeleted, ) } @@ -39,7 +39,6 @@ class AccountMapper @Inject constructor( icon = icon?.id, orderNum = orderNum, includeInBalance = includeInBalance, - isDeleted = removed, id = id.value, isSynced = true, // TODO: Delete this ) diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/CategoryMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/CategoryMapper.kt index 4c43c534dc..f63472a777 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/CategoryMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/CategoryMapper.kt @@ -2,36 +2,34 @@ package com.ivy.data.repository.mapper import arrow.core.Either import arrow.core.raise.either +import arrow.core.raise.ensure import com.ivy.data.db.entity.CategoryEntity import com.ivy.data.model.Category -import com.ivy.data.model.CategoryId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString -import java.time.Instant import javax.inject.Inject class CategoryMapper @Inject constructor() { - fun CategoryEntity.toDomain(): Either = either { - com.ivy.data.model.Category( + fun CategoryEntity.toDomain(): Either = either { + ensure(!isDeleted) { "Category is deleted" } + + Category( id = com.ivy.data.model.CategoryId(id), name = NotBlankTrimmedString.from(name).bind(), color = ColorInt(color), icon = icon?.let(IconAsset::from)?.getOrNull(), orderNum = orderNum, - lastUpdated = Instant.EPOCH, - removed = isDeleted ) } - fun com.ivy.data.model.Category.toEntity(): CategoryEntity { + fun Category.toEntity(): CategoryEntity { return CategoryEntity( name = name.value, color = color.value, icon = icon?.id, orderNum = orderNum, isSynced = true, - isDeleted = removed, id = id.value ) } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt index 21a91c3934..d2e5b8d7f6 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TagMapper.kt @@ -30,8 +30,6 @@ class TagMapper @Inject constructor() { icon = icon?.let(IconAsset::from)?.getOrNull(), orderNum = orderNum, creationTimestamp = dateTime, - lastUpdated = lastSyncedTime, - removed = isDeleted ) } @@ -44,8 +42,8 @@ class TagMapper @Inject constructor() { icon = icon?.id, orderNum = orderNum, dateTime = creationTimestamp, - lastSyncedTime = lastUpdated, - isDeleted = removed + lastSyncedTime = Instant.EPOCH, + isDeleted = false, ) } @@ -53,8 +51,8 @@ class TagMapper @Inject constructor() { return TagAssociationEntity( tagId = id.value, associatedId = associatedId.value, - lastSyncedTime = lastUpdated, - isDeleted = removed + lastSyncedTime = Instant.EPOCH, + isDeleted = false, ) } @@ -62,8 +60,6 @@ class TagMapper @Inject constructor() { return TagAssociation( id = TagId(tagId), associatedId = AssociationId(associatedId), - lastUpdated = lastSyncedTime, - removed = isDeleted ) } @@ -76,8 +72,6 @@ class TagMapper @Inject constructor() { icon = null, orderNum = 0.0, creationTimestamp = Instant.now(), - lastUpdated = Instant.EPOCH, - removed = false ) } @@ -85,8 +79,6 @@ class TagMapper @Inject constructor() { return TagAssociation( id = tagId, associatedId = associationId, - lastUpdated = Instant.EPOCH, - removed = false ) } } \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt index 0004813c82..2089678c73 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt @@ -34,6 +34,8 @@ class TransactionMapper @Inject constructor( suspend fun TransactionEntity.toDomain( tags: List = emptyList() ): Either = either { + ensure(!isDeleted) { "Transaction is deleted" } + val metadata = TransactionMetadata( recurringRuleId = recurringRuleId, paidForDateTime = paidForDateTime?.toInstant(ZoneOffset.UTC), @@ -69,8 +71,6 @@ class TransactionMapper @Inject constructor( time = time, settled = settled, metadata = metadata, - lastUpdated = Instant.EPOCH, - removed = isDeleted, tags = tags, ) } @@ -86,8 +86,6 @@ class TransactionMapper @Inject constructor( time = time, settled = settled, metadata = metadata, - lastUpdated = Instant.EPOCH, - removed = isDeleted, tags = tags, ) } @@ -121,8 +119,6 @@ class TransactionMapper @Inject constructor( time = time, settled = settled, metadata = metadata, - lastUpdated = Instant.EPOCH, - removed = isDeleted, fromAccount = accountId, fromValue = fromValue, toAccount = toAccountId, @@ -164,13 +160,14 @@ class TransactionMapper @Inject constructor( dateTime = dateTime.takeIf { settled }, categoryId = category?.value, dueDate = dateTime.takeIf { !settled }, - paidForDateTime = metadata.paidForDateTime?.atZone(timeProvider.getZoneId())?.toLocalDateTime(), + paidForDateTime = metadata.paidForDateTime?.atZone(timeProvider.getZoneId()) + ?.toLocalDateTime(), recurringRuleId = metadata.recurringRuleId, attachmentUrl = null, loanId = metadata.loanId, loanRecordId = metadata.loanRecordId, isSynced = true, - isDeleted = removed, + isDeleted = false, id = id.value ) } diff --git a/shared/data/core/src/test/java/com/ivy/data/ArbAccountEntity.kt b/shared/data/core/src/test/java/com/ivy/data/ArbAccountEntity.kt index 69dca59ab7..fbba02b313 100644 --- a/shared/data/core/src/test/java/com/ivy/data/ArbAccountEntity.kt +++ b/shared/data/core/src/test/java/com/ivy/data/ArbAccountEntity.kt @@ -30,7 +30,7 @@ fun Arb.Companion.validAccountEntity(): Arb = arbitrary { orderNum = Arb.double().removeEdgecases().bind(), includeInBalance = Arb.boolean().bind(), isSynced = Arb.boolean().bind(), - isDeleted = Arb.boolean().bind(), + isDeleted = false, id = Arb.uuid().bind() ) } \ No newline at end of file diff --git a/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt b/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt index 2010b5cd92..e5e3d1e3a2 100644 --- a/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt +++ b/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt @@ -83,7 +83,7 @@ fun Arb.Companion.validTransfer(): Arb = arbitrary { loanId = Arb.maybe(Arb.uuid()).bind(), loanRecordId = Arb.maybe(Arb.uuid()).bind(), isSynced = Arb.boolean().bind(), - isDeleted = Arb.boolean().bind(), + isDeleted = false, id = Arb.uuid().bind() ) } @@ -140,7 +140,7 @@ fun Arb.Companion.validIncomeOrExpense(): Arb = arbitrary { loanId = Arb.maybe(Arb.uuid()).bind(), loanRecordId = Arb.maybe(Arb.uuid()).bind(), isSynced = Arb.boolean().bind(), - isDeleted = Arb.boolean().bind(), + isDeleted = false, id = Arb.uuid().bind() ) } diff --git a/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt b/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt index 9ab5f19c5a..f8d7dd5de9 100644 --- a/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/backup/BackupDataUseCaseTest.kt @@ -11,8 +11,9 @@ import com.ivy.data.db.dao.fake.FakeLoanRecordDao import com.ivy.data.db.dao.fake.FakePlannedPaymentDao import com.ivy.data.db.dao.fake.FakeSettingsDao import com.ivy.data.db.dao.fake.FakeTransactionDao -import com.ivy.data.repository.fake.FakeAccountRepository -import com.ivy.data.repository.fake.FakeCurrencyRepository +import com.ivy.data.repository.AccountRepository +import com.ivy.data.repository.CurrencyRepository +import com.ivy.data.repository.fake.fakeRepositoryMemoFactory import com.ivy.data.repository.mapper.AccountMapper import com.ivy.data.testResource import io.kotest.matchers.ints.shouldBeGreaterThan @@ -31,42 +32,47 @@ class BackupDataUseCaseTest { settingsDao: FakeSettingsDao = FakeSettingsDao(), loanDao: FakeLoanDao = FakeLoanDao(), loanRecordDao: FakeLoanRecordDao = FakeLoanRecordDao(), - ): BackupDataUseCase = BackupDataUseCase( - accountDao = accountDao, - accountMapper = AccountMapper( - FakeCurrencyRepository( + ): BackupDataUseCase { + val accountMapper = AccountMapper( + CurrencyRepository( settingsDao = settingsDao, - writeSettingsDao = settingsDao + writeSettingsDao = settingsDao, + dispatchersProvider = TestDispatchersProvider, ) - ), - accountRepository = FakeAccountRepository( + ) + return BackupDataUseCase( accountDao = accountDao, - writeAccountDao = accountDao, + accountMapper = accountMapper, + accountRepository = AccountRepository( + accountDao = accountDao, + writeAccountDao = accountDao, + mapper = accountMapper, + dispatchersProvider = TestDispatchersProvider, + memoFactory = fakeRepositoryMemoFactory(), + ), + budgetDao = budgetDao, + categoryDao = categoryDao, + loanRecordDao = loanRecordDao, + loanDao = loanDao, + plannedPaymentRuleDao = plannedPaymentDao, + transactionDao = transactionDao, + transactionWriter = transactionDao, settingsDao = settingsDao, - writeSettingsDao = settingsDao - ), - budgetDao = budgetDao, - categoryDao = categoryDao, - loanRecordDao = loanRecordDao, - loanDao = loanDao, - plannedPaymentRuleDao = plannedPaymentDao, - transactionDao = transactionDao, - transactionWriter = transactionDao, - settingsDao = settingsDao, - categoryWriter = categoryDao, - settingsWriter = settingsDao, - budgetWriter = budgetDao, - loanWriter = loanDao, - loanRecordWriter = loanRecordDao, - plannedPaymentRuleWriter = plannedPaymentDao, + categoryWriter = categoryDao, + settingsWriter = settingsDao, + budgetWriter = budgetDao, + loanWriter = loanDao, + loanRecordWriter = loanRecordDao, + plannedPaymentRuleWriter = plannedPaymentDao, - context = mockk(relaxed = true), - sharedPrefs = mockk(relaxed = true), - json = KotlinxSerializationModule.provideJson(), - dispatchersProvider = TestDispatchersProvider, - fileSystem = mockk(relaxed = true), - dataObserver = DataObserver(), - ) + context = mockk(relaxed = true), + sharedPrefs = mockk(relaxed = true), + json = KotlinxSerializationModule.provideJson(), + dispatchersProvider = TestDispatchersProvider, + fileSystem = mockk(relaxed = true), + dataObserver = DataObserver(), + ) + } private suspend fun backupTestCase(backupVersion: String) { // given diff --git a/shared/data/core/src/test/java/com/ivy/data/dao/FakeAccountDaoTest.kt b/shared/data/core/src/test/java/com/ivy/data/dao/FakeAccountDaoTest.kt index b6ff7d679f..cc74498a37 100644 --- a/shared/data/core/src/test/java/com/ivy/data/dao/FakeAccountDaoTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/dao/FakeAccountDaoTest.kt @@ -302,32 +302,6 @@ class FakeAccountDaoTest { res shouldBe accounts } - @Test - fun `flag deleted`() = runTest { - // given - val id = UUID.randomUUID() - val account = AccountEntity( - id = id, - name = "Bank", - currency = "BGN", - color = 1, - icon = null, - includeInBalance = true, - orderNum = 1.0, - isDeleted = false - ) - - // when - dao.save(account) - dao.flagDeleted(id) - val notDeleted = dao.findAll(false) - val deleted = dao.findAll(true) - - // then - notDeleted shouldBe emptyList() - deleted shouldBe listOf(account.copy(isDeleted = true)) - } - @Test fun `delete by id`() = runTest { // given diff --git a/shared/data/core/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt b/shared/data/core/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt index 3fbd687a53..22e0e07f3f 100644 --- a/shared/data/core/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt @@ -298,50 +298,6 @@ class FakeCategoryDaoTest { res shouldBe categories } - @Test - fun `flag deleted - existing id`() = runTest { - // given - val id = UUID.randomUUID() - val category = CategoryEntity( - name = "Home", - color = 42, - icon = null, - orderNum = 1.0, - isDeleted = false, - id = id - ) - - // when - dao.save(category) - dao.flagDeleted(id) - val res = dao.findById(id) - - // then - res shouldBe category.copy(isDeleted = true) - } - - @Test - fun `flag deleted - not existing id`() = runTest { - // given - val id = UUID.randomUUID() - val category = CategoryEntity( - name = "Home", - color = 42, - icon = null, - orderNum = 1.0, - isDeleted = false, - id = id - ) - - // when - dao.save(category) - dao.flagDeleted(UUID.randomUUID()) - val res = dao.findById(id) - - // then - res shouldBe category - } - @Test fun `delete by id`() = runTest { // given diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/AccountRepositoryTest.kt similarity index 90% rename from shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt rename to shared/data/core/src/test/java/com/ivy/data/repository/AccountRepositoryTest.kt index fb1b0a07f8..c065322aad 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/impl/AccountRepositoryImplTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/AccountRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.ivy.data.repository.impl +package com.ivy.data.repository import com.ivy.base.TestDispatchersProvider import com.ivy.data.DataObserver @@ -11,9 +11,7 @@ import com.ivy.data.model.AccountId import com.ivy.data.model.primitive.AssetCode import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.repository.AccountRepository -import com.ivy.data.repository.fake.FakeCurrencyRepository -import com.ivy.data.repository.fake.fakeRepositoryMakeFactory +import com.ivy.data.repository.fake.fakeRepositoryMemoFactory import com.ivy.data.repository.mapper.AccountMapper import io.kotest.matchers.shouldBe import io.mockk.coEvery @@ -24,10 +22,9 @@ import io.mockk.runs import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import java.time.Instant import java.util.UUID -class AccountRepositoryImplTest { +class AccountRepositoryTest { val accountDao = mockk() val writeAccountDao = mockk() val writeEventBus = mockk(relaxed = true) @@ -37,12 +34,18 @@ class AccountRepositoryImplTest { @Before fun setup() { val settingsDao = FakeSettingsDao() - repository = AccountRepositoryImpl( - mapper = AccountMapper(FakeCurrencyRepository(settingsDao, settingsDao)), + repository = AccountRepository( + mapper = AccountMapper( + CurrencyRepository( + settingsDao = settingsDao, + writeSettingsDao = settingsDao, + dispatchersProvider = TestDispatchersProvider + ) + ), accountDao = accountDao, writeAccountDao = writeAccountDao, dispatchersProvider = TestDispatchersProvider, - memoFactory = fakeRepositoryMakeFactory(), + memoFactory = fakeRepositoryMemoFactory(), ) } @@ -87,8 +90,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 1.0, - lastUpdated = Instant.EPOCH, - removed = false ) } @@ -121,7 +122,7 @@ class AccountRepositoryImplTest { coEvery { accountDao.findAll(false) } returns emptyList() // when - val res = repository.findAll(false) + val res = repository.findAll() // then res shouldBe emptyList() @@ -158,7 +159,7 @@ class AccountRepositoryImplTest { ) // when - val res = repository.findAll(false) + val res = repository.findAll() // then res shouldBe listOf( @@ -170,8 +171,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 1.0, - lastUpdated = Instant.EPOCH, - removed = false ), Account( id = account2Id, @@ -181,8 +180,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 2.0, - lastUpdated = Instant.EPOCH, - removed = false ) ) } @@ -218,7 +215,7 @@ class AccountRepositoryImplTest { ) // when - val res = repository.findAll(false) + val res = repository.findAll() // then res shouldBe listOf( @@ -230,8 +227,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 1.0, - lastUpdated = Instant.EPOCH, - removed = false ) ) } @@ -273,8 +268,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 1.0, - lastUpdated = Instant.EPOCH, - removed = false ) // when @@ -313,8 +306,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 1.0, - lastUpdated = Instant.EPOCH, - removed = false ), Account( id = account2Id, @@ -324,8 +315,6 @@ class AccountRepositoryImplTest { icon = null, includeInBalance = true, orderNum = 2.0, - lastUpdated = Instant.EPOCH, - removed = false ) ) diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/CategoryRepositoryTest.kt similarity index 90% rename from shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt rename to shared/data/core/src/test/java/com/ivy/data/repository/CategoryRepositoryTest.kt index 38f24e561f..f4a6a2b3bc 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/impl/CategoryRepositoryImplTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/CategoryRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.ivy.data.repository.impl +package com.ivy.data.repository import com.ivy.base.TestDispatchersProvider import com.ivy.data.db.dao.read.CategoryDao @@ -8,8 +8,7 @@ import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.repository.CategoryRepository -import com.ivy.data.repository.fake.fakeRepositoryMakeFactory +import com.ivy.data.repository.fake.fakeRepositoryMemoFactory import com.ivy.data.repository.mapper.CategoryMapper import io.kotest.matchers.shouldBe import io.mockk.coEvery @@ -20,10 +19,9 @@ import io.mockk.runs import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import java.time.Instant import java.util.UUID -class CategoryRepositoryImplTest { +class CategoryRepositoryTest { private val categoryDao = mockk() private val writeCategoryDao = mockk() @@ -31,12 +29,12 @@ class CategoryRepositoryImplTest { @Before fun setup() { - repository = CategoryRepositoryImpl( + repository = CategoryRepository( mapper = CategoryMapper(), categoryDao = categoryDao, writeCategoryDao = writeCategoryDao, dispatchersProvider = TestDispatchersProvider, - memoFactory = fakeRepositoryMakeFactory(), + memoFactory = fakeRepositoryMemoFactory(), ) } @@ -46,7 +44,7 @@ class CategoryRepositoryImplTest { coEvery { categoryDao.findAll(false) } returns emptyList() // when - val res = repository.findAll(false) + val res = repository.findAll() // then res shouldBe emptyList() @@ -88,7 +86,7 @@ class CategoryRepositoryImplTest { ) // when - val res = repository.findAll(false) + val res = repository.findAll() // then res shouldBe listOf( @@ -97,8 +95,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 0.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id1) ), Category( @@ -106,8 +102,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 2.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id3) ) ) @@ -149,8 +143,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 0.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id) ) } @@ -209,8 +201,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 3.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id) ) coEvery { writeCategoryDao.save(any()) } just runs @@ -246,8 +236,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 3.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id1) ), Category( @@ -255,8 +243,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 4.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id2) ), Category( @@ -264,8 +250,6 @@ class CategoryRepositoryImplTest { color = ColorInt(42), icon = null, orderNum = 5.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId(id3) ) ) diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/ExchangeRatesRepositoryTest.kt similarity index 93% rename from shared/data/core/src/test/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImplTest.kt rename to shared/data/core/src/test/java/com/ivy/data/repository/ExchangeRatesRepositoryTest.kt index dadeb4ec86..8ed2b67454 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/impl/ExchangeRatesRepositoryImplTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/ExchangeRatesRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.ivy.data.repository.impl +package com.ivy.data.repository import arrow.core.Either import arrow.core.left @@ -8,7 +8,6 @@ import com.ivy.data.db.dao.write.WriteExchangeRatesDao import com.ivy.data.model.ExchangeRate import com.ivy.data.remote.RemoteExchangeRatesDataSource import com.ivy.data.remote.responses.ExchangeRatesResponse -import com.ivy.data.repository.ExchangeRatesRepository import com.ivy.data.repository.mapper.ExchangeRateMapper import io.kotest.assertions.arrow.core.shouldBeLeft import io.kotest.assertions.arrow.core.shouldBeRight @@ -20,7 +19,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -class ExchangeRatesRepositoryImplTest { +class ExchangeRatesRepositoryTest { private val mapper = mockk() private val exchangeRatesDao = mockk() private val writeExchangeRatesDao = mockk() @@ -30,7 +29,7 @@ class ExchangeRatesRepositoryImplTest { @Before fun setup() { - repository = ExchangeRatesRepositoryImpl( + repository = ExchangeRatesRepository( mapper = mapper, exchangeRatesDao = exchangeRatesDao, writeExchangeRatesDao = writeExchangeRatesDao, diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/TransactionRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/TransactionRepositoryTest.kt similarity index 92% rename from shared/data/core/src/test/java/com/ivy/data/repository/impl/TransactionRepositoryImplTest.kt rename to shared/data/core/src/test/java/com/ivy/data/repository/TransactionRepositoryTest.kt index 66c30e6e1d..2d308a9838 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/impl/TransactionRepositoryImplTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/TransactionRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.ivy.data.repository.impl +package com.ivy.data.repository import arrow.core.Either import arrow.core.Some @@ -19,8 +19,6 @@ import com.ivy.data.model.testing.ModelFixtures import com.ivy.data.model.testing.accountId import com.ivy.data.model.testing.transaction import com.ivy.data.model.testing.transactionId -import com.ivy.data.repository.TagRepository -import com.ivy.data.repository.TransactionRepository import com.ivy.data.repository.mapper.TransactionMapper import com.ivy.data.validTransactionEntity import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual @@ -41,9 +39,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -typealias TrnMappingRow = Pair> - -class TransactionRepositoryImplTest { +class TransactionRepositoryTest { private val mapper = mockk() private val transactionDao = mockk() @@ -59,7 +55,7 @@ class TransactionRepositoryImplTest { private fun newRepository( fakeDao: FakeTransactionDao?, - ): TransactionRepository = TransactionRepositoryImpl( + ): TransactionRepository = TransactionRepository( mapper = mapper, transactionDao = fakeDao ?: transactionDao, writeTransactionDao = fakeDao ?: writeTransactionDao, @@ -405,37 +401,6 @@ class TransactionRepositoryImplTest { savedTrns.toSet() shouldBe setOf(trn1, trn2) } - @Test - fun flagDeletedByAccountId() = runTest { - val accountId = ModelFixtures.AccountId - // given - repository = newRepository(fakeDao = FakeTransactionDao()) - val trn = mockkFakeTrnMapping(account = accountId) - repository.save(trn) - - // when - repository.flagDeletedByAccountId(accountId.value) - - // then - repository.findAllIncomeByAccount(accountId) shouldBe emptyList() - repository.findAllExpenseByAccount(accountId) shouldBe emptyList() - repository.findAllTransferByAccount(accountId) shouldBe emptyList() - } - - @Test - fun flagDeleted() = runTest { - // given - repository = newRepository(fakeDao = FakeTransactionDao()) - val trn = mockkFakeTrnMapping() - repository.save(trn) - - // when - repository.flagDeleted(trn.id) - - // then - repository.findById(trn.id) shouldBe null - } - @Test fun deleteById() = runTest { // given @@ -544,4 +509,6 @@ class TransactionRepositoryImplTest { Arb.invalidTransactionEntity().bind() to Either.Left(Arb.string().bind()) } } -} \ No newline at end of file +} + +typealias TrnMappingRow = Pair> \ No newline at end of file diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt index 94a5290573..2f610d0bc1 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import java.time.Instant import java.util.UUID @RunWith(TestParameterInjector::class) @@ -38,7 +37,6 @@ class AccountMapperTest { @Test fun `maps domain to entity`( @TestParameter includeInBalance: Boolean, - @TestParameter removed: Boolean, ) { // given val account = Account( @@ -49,8 +47,6 @@ class AccountMapperTest { icon = IconAsset.unsafe("icon"), includeInBalance = includeInBalance, orderNum = 3.14, - lastUpdated = Instant.EPOCH, - removed = removed ) // when @@ -65,7 +61,7 @@ class AccountMapperTest { includeInBalance = includeInBalance, orderNum = 3.14, isSynced = true, - isDeleted = removed, + isDeleted = false, id = ModelFixtures.AccountId.value, ) } @@ -87,17 +83,19 @@ class AccountMapperTest { val result = with(mapper) { entity.toDomain() } // then - result.shouldBeRight() shouldBe Account( - id = AccountId(entity.id), - name = NotBlankTrimmedString.unsafe("Test"), - asset = AssetCode.unsafe("USD"), - color = ColorInt(value = 42), - icon = IconAsset.unsafe("icon"), - includeInBalance = includeInBalance, - orderNum = 42.0, - lastUpdated = Instant.EPOCH, - removed = removed - ) + if (removed) { + result.shouldBeLeft() + } else { + result.shouldBeRight() shouldBe Account( + id = AccountId(entity.id), + name = NotBlankTrimmedString.unsafe("Test"), + asset = AssetCode.unsafe("USD"), + color = ColorInt(value = 42), + icon = IconAsset.unsafe("icon"), + includeInBalance = includeInBalance, + orderNum = 42.0, + ) + } } @Test diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/CategoryMapperTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/CategoryMapperTest.kt index 25aea0ff91..e536e3c971 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/CategoryMapperTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/CategoryMapperTest.kt @@ -1,7 +1,6 @@ package com.ivy.data.repository.mapper import com.ivy.data.db.entity.CategoryEntity -import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.NotBlankTrimmedString @@ -10,7 +9,6 @@ import io.kotest.assertions.arrow.core.shouldBeRight import io.kotest.matchers.shouldBe import org.junit.Before import org.junit.Test -import java.time.Instant import java.util.UUID class CategoryMapperTest { @@ -29,8 +27,6 @@ class CategoryMapperTest { color = ColorInt(42), icon = null, orderNum = 1.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId ) @@ -60,8 +56,6 @@ class CategoryMapperTest { color = ColorInt(42), icon = null, orderNum = 1.0, - removed = false, - lastUpdated = Instant.EPOCH, id = CategoryId ) } @@ -78,6 +72,18 @@ class CategoryMapperTest { res.shouldBeLeft() } + @Test + fun `maps entity to domain - deleted categories are not valid`() { + // given + val corruptedEntity = ValidEntity.copy(isDeleted = true) + + // when + val res = with(mapper) { corruptedEntity.toDomain() } + + // then + res.shouldBeLeft() + } + @Test fun `maps entity to domain - missing icon is okay`() { // given @@ -103,7 +109,7 @@ class CategoryMapperTest { } companion object { - val CategoryId = com.ivy.data.model.CategoryId(UUID.randomUUID()) + val CategoryId = CategoryId(UUID.randomUUID()) val ValidEntity = CategoryEntity( name = "Home", diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt index 5feaa7fc3c..5c983d4dcb 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt @@ -10,10 +10,10 @@ import com.ivy.data.model.AccountId import com.ivy.data.model.CategoryId import com.ivy.data.model.Expense import com.ivy.data.model.Income +import com.ivy.data.model.PositiveValue import com.ivy.data.model.TransactionId import com.ivy.data.model.TransactionMetadata import com.ivy.data.model.Transfer -import com.ivy.data.model.PositiveValue import com.ivy.data.model.primitive.AssetCode import com.ivy.data.model.primitive.AssetCode.Companion.EUR import com.ivy.data.model.primitive.AssetCode.Companion.USD @@ -61,7 +61,6 @@ class TransactionMapperTest { @Test fun `maps domain income to entity`( @TestParameter settled: Boolean, - @TestParameter removed: Boolean, ) { // given val income = Income( @@ -77,8 +76,6 @@ class TransactionMapperTest { paidForDateTime = PaidForDateTime, loanRecordId = LoanRecordId ), - lastUpdated = InstantNow, - removed = removed, value = PositiveValue( amount = PositiveDouble.unsafe(100.0), asset = AssetCode.unsafe("NGN") @@ -109,7 +106,7 @@ class TransactionMapperTest { loanId = LoanId, loanRecordId = LoanRecordId, isSynced = true, - isDeleted = removed, + isDeleted = false, id = TransactionId.value ) } @@ -117,7 +114,6 @@ class TransactionMapperTest { @Test fun `maps domain expense to entity`( @TestParameter settled: Boolean, - @TestParameter removed: Boolean, ) { // given val expense = Expense( @@ -133,8 +129,6 @@ class TransactionMapperTest { paidForDateTime = PaidForDateTime, loanRecordId = LoanRecordId ), - lastUpdated = Instant.EPOCH, - removed = removed, value = PositiveValue( amount = PositiveDouble.unsafe(100.0), asset = AssetCode.unsafe("NGN") @@ -165,7 +159,7 @@ class TransactionMapperTest { loanId = LoanId, loanRecordId = LoanRecordId, isSynced = true, - isDeleted = removed, + isDeleted = false, id = TransactionId.value ) } @@ -173,7 +167,6 @@ class TransactionMapperTest { @Test fun `maps domain transfer to entity`( @TestParameter settled: Boolean, - @TestParameter removed: Boolean, ) { // given val transfer = Transfer( @@ -189,8 +182,6 @@ class TransactionMapperTest { paidForDateTime = PaidForDateTime, loanRecordId = LoanRecordId ), - lastUpdated = Instant.EPOCH, - removed = removed, fromValue = PositiveValue( amount = PositiveDouble.unsafe(100.0), asset = AssetCode.unsafe("NGN") @@ -226,7 +217,7 @@ class TransactionMapperTest { loanId = LoanId, loanRecordId = LoanRecordId, isSynced = true, - isDeleted = removed, + isDeleted = false, id = TransactionId.value ) } @@ -250,28 +241,30 @@ class TransactionMapperTest { val income = with(mapper) { entity.toDomain() } // then - income.shouldBeRight() shouldBe Income( - id = TransactionId, - title = NotBlankTrimmedString.unsafe("Income"), - description = NotBlankTrimmedString.unsafe("Income desc"), - category = CategoryId, - time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), - settled = settled, - metadata = TransactionMetadata( - recurringRuleId = RecurringRuleId, - loanId = LoanId, - paidForDateTime = PaidForDateTime, - loanRecordId = LoanRecordId - ), - lastUpdated = Instant.EPOCH, - removed = removed, - value = PositiveValue( - amount = PositiveDouble.unsafe(100.0), - asset = EUR - ), - account = AccountId, - tags = persistentListOf() - ) + if (removed) { + income.shouldBeLeft() + } else { + income.shouldBeRight() shouldBe Income( + id = TransactionId, + title = NotBlankTrimmedString.unsafe("Income"), + description = NotBlankTrimmedString.unsafe("Income desc"), + category = CategoryId, + time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), + settled = settled, + metadata = TransactionMetadata( + recurringRuleId = RecurringRuleId, + loanId = LoanId, + paidForDateTime = PaidForDateTime, + loanRecordId = LoanRecordId + ), + value = PositiveValue( + amount = PositiveDouble.unsafe(100.0), + asset = EUR + ), + account = AccountId, + tags = persistentListOf() + ) + } } @Test @@ -377,28 +370,30 @@ class TransactionMapperTest { val expense = with(mapper) { entity.toDomain() } // then - expense.shouldBeRight() shouldBe Expense( - id = TransactionId, - title = NotBlankTrimmedString.unsafe("Expense"), - description = NotBlankTrimmedString.unsafe("Expense desc"), - category = CategoryId, - time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), - settled = settled, - metadata = TransactionMetadata( - recurringRuleId = RecurringRuleId, - loanId = LoanId, - paidForDateTime = PaidForDateTime, - loanRecordId = LoanRecordId - ), - lastUpdated = Instant.EPOCH, - removed = removed, - value = PositiveValue( - amount = PositiveDouble.unsafe(100.0), - asset = EUR - ), - account = AccountId, - tags = persistentListOf() - ) + if (removed) { + expense.shouldBeLeft() + } else { + expense.shouldBeRight() shouldBe Expense( + id = TransactionId, + title = NotBlankTrimmedString.unsafe("Expense"), + description = NotBlankTrimmedString.unsafe("Expense desc"), + category = CategoryId, + time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), + settled = settled, + metadata = TransactionMetadata( + recurringRuleId = RecurringRuleId, + loanId = LoanId, + paidForDateTime = PaidForDateTime, + loanRecordId = LoanRecordId + ), + value = PositiveValue( + amount = PositiveDouble.unsafe(100.0), + asset = EUR + ), + account = AccountId, + tags = persistentListOf() + ) + } } @Test @@ -509,33 +504,35 @@ class TransactionMapperTest { val transfer = with(mapper) { entity.toDomain() } // then - transfer.shouldBeRight() shouldBe Transfer( - id = TransactionId, - title = NotBlankTrimmedString.unsafe("Transfer"), - description = NotBlankTrimmedString.unsafe("Transfer desc"), - category = CategoryId, - time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), - settled = settled, - metadata = TransactionMetadata( - recurringRuleId = RecurringRuleId, - loanId = LoanId, - paidForDateTime = PaidForDateTime, - loanRecordId = LoanRecordId - ), - lastUpdated = Instant.EPOCH, - removed = removed, - fromValue = PositiveValue( - amount = PositiveDouble.unsafe(50.0), - asset = EUR - ), - fromAccount = AccountId, - toValue = PositiveValue( - amount = PositiveDouble.unsafe(55.0), - asset = USD - ), - toAccount = ToAccountId, - tags = persistentListOf() - ) + if (removed) { + transfer.shouldBeLeft() + } else { + transfer.shouldBeRight() shouldBe Transfer( + id = TransactionId, + title = NotBlankTrimmedString.unsafe("Transfer"), + description = NotBlankTrimmedString.unsafe("Transfer desc"), + category = CategoryId, + time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), + settled = settled, + metadata = TransactionMetadata( + recurringRuleId = RecurringRuleId, + loanId = LoanId, + paidForDateTime = PaidForDateTime, + loanRecordId = LoanRecordId + ), + fromValue = PositiveValue( + amount = PositiveDouble.unsafe(50.0), + asset = EUR + ), + fromAccount = AccountId, + toValue = PositiveValue( + amount = PositiveDouble.unsafe(55.0), + asset = USD + ), + toAccount = ToAccountId, + tags = persistentListOf() + ) + } } @Test diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt index fda1c0f987..0224696bea 100644 --- a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt @@ -12,11 +12,9 @@ import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.double import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.uuid -import java.time.Instant fun Arb.Companion.account( accountId: Option = None, - removed: Option = None, asset: Option = None, includeInBalance: Option = None, orderNum: Option = None, @@ -29,8 +27,6 @@ fun Arb.Companion.account( icon = Arb.maybe(Arb.iconAsset()).bind(), includeInBalance = includeInBalance.getOrElse { Arb.boolean().bind() }, orderNum = orderNum.getOrElse { Arb.double().bind() }, - lastUpdated = Instant.EPOCH, - removed = removed.getOrElse { Arb.boolean().bind() } ) } diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt index 285fbc97ed..b6983d510d 100644 --- a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt @@ -7,9 +7,7 @@ import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import io.kotest.property.Arb import io.kotest.property.arbitrary.arbitrary -import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.double -import io.kotest.property.arbitrary.instant import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.removeEdgecases import io.kotest.property.arbitrary.uuid @@ -23,8 +21,6 @@ fun Arb.Companion.category( color = Arb.colorInt().bind(), icon = Arb.maybe(Arb.iconAsset()).bind(), orderNum = Arb.double().removeEdgecases().bind(), - lastUpdated = Arb.instant().bind(), - removed = Arb.boolean().bind() ) } diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt index 6f8c1380c1..9a47f5680b 100644 --- a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt @@ -32,7 +32,6 @@ fun Arb.Companion.income( categoryId: Option = None, settled: Option = None, time: Option = None, - removed: Option = Some(false), amount: Option = None, asset: Option = None, id: Option = None @@ -50,8 +49,6 @@ fun Arb.Companion.income( paidForDateTime = null, loanRecordId = null ), - lastUpdated = Instant.EPOCH, - removed = removed.getOrElse { Arb.boolean().bind() }, tags = listOf(), value = Arb.value(amount, asset).bind(), account = accountId.getOrElse { Arb.accountId().bind() } @@ -63,7 +60,6 @@ fun Arb.Companion.expense( categoryId: Option = None, settled: Option = None, time: Option = None, - removed: Option = Some(false), amount: Option = None, asset: Option = None, id: Option = None @@ -81,8 +77,6 @@ fun Arb.Companion.expense( paidForDateTime = null, loanRecordId = null ), - lastUpdated = Instant.EPOCH, - removed = removed.getOrElse { Arb.boolean().bind() }, tags = listOf(), value = Arb.value(amount, asset).bind(), account = accountId.getOrElse { Arb.accountId().bind() } @@ -93,7 +87,6 @@ fun Arb.Companion.transfer( categoryId: Option = None, settled: Option = None, time: Option = None, - removed: Option = Some(false), fromAccount: Option = None, fromAmount: Option = None, fromAsset: Option = None, @@ -116,8 +109,6 @@ fun Arb.Companion.transfer( paidForDateTime = null, loanRecordId = null ), - lastUpdated = Instant.EPOCH, - removed = removed.getOrElse { Arb.boolean().bind() }, tags = listOf(), fromValue = Arb.value(fromAmount, fromAsset).bind(), fromAccount = fromAccountVal, diff --git a/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt b/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt index 36bcfd54bc..ae0983343c 100644 --- a/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt +++ b/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt @@ -29,7 +29,6 @@ class ArbTransactionTest { @Test fun `arb income respects passed params`( @TestParameter settled: Boolean, - @TestParameter removed: Boolean, ) = runTest { // given val transactionId = ModelFixtures.TransactionId @@ -46,7 +45,6 @@ class ArbTransactionTest { categoryId = Some(categoryId), settled = Some(settled), time = Some(ArbTime.Exactly(now)), - removed = Some(removed), amount = Some(amount), asset = Some(asset), id = Some(transactionId), @@ -57,7 +55,6 @@ class ArbTransactionTest { income.account shouldBe accountId income.category shouldBe categoryId income.settled shouldBe settled - income.removed shouldBe removed income.time shouldBe now income.value shouldBe PositiveValue(amount, asset) } @@ -73,7 +70,6 @@ class ArbTransactionTest { @Test fun `arb expense respects passed params`( @TestParameter settled: Boolean, - @TestParameter removed: Boolean, ) = runTest { // given val transactionId = ModelFixtures.TransactionId @@ -90,7 +86,6 @@ class ArbTransactionTest { categoryId = Some(categoryId), settled = Some(settled), time = Some(ArbTime.Exactly(now)), - removed = Some(removed), amount = Some(amount), asset = Some(asset), id = Some(transactionId), @@ -101,7 +96,6 @@ class ArbTransactionTest { expense.account shouldBe accountId expense.category shouldBe categoryId expense.settled shouldBe settled - expense.removed shouldBe removed expense.time shouldBe now expense.value shouldBe PositiveValue(amount, asset) } @@ -117,7 +111,6 @@ class ArbTransactionTest { @Test fun `arb transfer respects passed params`( @TestParameter settled: Boolean, - @TestParameter removed: Boolean, ) = runTest { // given val transactionId = ModelFixtures.TransactionId @@ -136,7 +129,6 @@ class ArbTransactionTest { categoryId = Some(categoryId), settled = Some(settled), time = Some(ArbTime.Exactly(now)), - removed = Some(removed), id = Some(transactionId), fromAccount = Some(fromAccount), fromAmount = Some(fromAmount), @@ -150,7 +142,6 @@ class ArbTransactionTest { transfer.id shouldBe transactionId transfer.category shouldBe categoryId transfer.settled shouldBe settled - transfer.removed shouldBe removed transfer.time shouldBe now transfer.fromAccount shouldBe fromAccount transfer.fromValue shouldBe PositiveValue(fromAmount, fromAsset) diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/Account.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/Account.kt index 4153297e2f..88d5391de5 100644 --- a/shared/data/model/src/main/kotlin/com/ivy/data/model/Account.kt +++ b/shared/data/model/src/main/kotlin/com/ivy/data/model/Account.kt @@ -4,9 +4,8 @@ import com.ivy.data.model.primitive.AssetCode import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.model.sync.Syncable +import com.ivy.data.model.sync.Identifiable import com.ivy.data.model.sync.UniqueId -import java.time.Instant import java.util.UUID @JvmInline @@ -20,6 +19,4 @@ data class Account( val icon: IconAsset?, val includeInBalance: Boolean, override val orderNum: Double, - override val lastUpdated: Instant, - override val removed: Boolean, -) : Syncable, Reorderable +) : Identifiable, Reorderable diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/Category.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/Category.kt index b679ad47e5..f66c26455f 100644 --- a/shared/data/model/src/main/kotlin/com/ivy/data/model/Category.kt +++ b/shared/data/model/src/main/kotlin/com/ivy/data/model/Category.kt @@ -3,9 +3,8 @@ package com.ivy.data.model import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.model.sync.Syncable +import com.ivy.data.model.sync.Identifiable import com.ivy.data.model.sync.UniqueId -import java.time.Instant import java.util.UUID @JvmInline @@ -17,6 +16,4 @@ data class Category( val color: ColorInt, val icon: IconAsset?, override val orderNum: Double, - override val lastUpdated: Instant, - override val removed: Boolean -) : Syncable, Reorderable \ No newline at end of file +) : Identifiable, Reorderable \ No newline at end of file diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/Tag.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/Tag.kt index c090aad73a..86aa93986a 100644 --- a/shared/data/model/src/main/kotlin/com/ivy/data/model/Tag.kt +++ b/shared/data/model/src/main/kotlin/com/ivy/data/model/Tag.kt @@ -5,7 +5,7 @@ import com.ivy.data.model.primitive.AssociationId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.model.sync.Syncable +import com.ivy.data.model.sync.Identifiable import com.ivy.data.model.sync.UniqueId import java.time.Instant import java.util.UUID @@ -18,17 +18,13 @@ data class Tag( val icon: IconAsset?, override val orderNum: Double, val creationTimestamp: Instant, - override val lastUpdated: Instant, - override val removed: Boolean -) : Syncable, Reorderable +) : Identifiable, Reorderable @Suppress("DataClassTypedIDs") data class TagAssociation( override val id: TagId, val associatedId: AssociationId, - override val lastUpdated: Instant, - override val removed: Boolean, -) : Syncable +) : Identifiable @JvmInline @Immutable diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt index b6464968e4..b6500210d4 100644 --- a/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt +++ b/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt @@ -1,7 +1,7 @@ package com.ivy.data.model import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.model.sync.Syncable +import com.ivy.data.model.sync.Identifiable import com.ivy.data.model.sync.UniqueId import java.time.Instant import java.util.UUID @@ -9,7 +9,7 @@ import java.util.UUID @JvmInline value class TransactionId(override val value: UUID) : UniqueId -sealed interface Transaction : Syncable { +sealed interface Transaction : Identifiable { override val id: TransactionId val title: NotBlankTrimmedString? val description: NotBlankTrimmedString? @@ -30,8 +30,6 @@ data class Income( override val time: Instant, override val settled: Boolean, override val metadata: TransactionMetadata, - override val lastUpdated: Instant, - override val removed: Boolean, override val tags: List, val value: PositiveValue, val account: AccountId, @@ -45,8 +43,6 @@ data class Expense( override val time: Instant, override val settled: Boolean, override val metadata: TransactionMetadata, - override val lastUpdated: Instant, - override val removed: Boolean, override val tags: List, val value: PositiveValue, val account: AccountId, @@ -60,8 +56,6 @@ data class Transfer( override val time: Instant, override val settled: Boolean, override val metadata: TransactionMetadata, - override val lastUpdated: Instant, - override val removed: Boolean, override val tags: List, val fromAccount: AccountId, val fromValue: PositiveValue, diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Identifiable.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Identifiable.kt new file mode 100644 index 0000000000..9b6b529e71 --- /dev/null +++ b/shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Identifiable.kt @@ -0,0 +1,5 @@ +package com.ivy.data.model.sync + +interface Identifiable { + val id: ID +} diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Syncable.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Syncable.kt deleted file mode 100644 index 62ba8bb91c..0000000000 --- a/shared/data/model/src/main/kotlin/com/ivy/data/model/sync/Syncable.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.ivy.data.model.sync - -import java.time.Instant - -interface Syncable { - val id: ID - val lastUpdated: Instant - - /** - * Tombstone flag - */ - val removed: Boolean -} diff --git a/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt b/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt index ded621141b..96694cc43a 100644 --- a/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt +++ b/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt @@ -3,10 +3,10 @@ package com.ivy.data.model.primitive import com.ivy.data.model.AccountId import com.ivy.data.model.Expense import com.ivy.data.model.Income +import com.ivy.data.model.PositiveValue import com.ivy.data.model.TransactionId import com.ivy.data.model.TransactionMetadata import com.ivy.data.model.Transfer -import com.ivy.data.model.PositiveValue import com.ivy.data.model.getFromAccount import com.ivy.data.model.getFromValue import com.ivy.data.model.getToAccount @@ -142,8 +142,6 @@ class TransactionTest { paidForDateTime = null, loanRecordId = null ), - lastUpdated = Instant.EPOCH, - removed = false, tags = listOf(), value = PositiveValue( amount = PositiveDouble.unsafe(1.0), @@ -165,8 +163,6 @@ class TransactionTest { paidForDateTime = null, loanRecordId = null ), - lastUpdated = Instant.EPOCH, - removed = false, tags = listOf(), value = PositiveValue( amount = PositiveDouble.unsafe(1.0), @@ -188,8 +184,6 @@ class TransactionTest { paidForDateTime = null, loanRecordId = null ), - lastUpdated = Instant.EPOCH, - removed = false, tags = listOf(), fromAccount = AccountId, fromValue = PositiveValue( diff --git a/shared/domain/src/androidTest/java/com/ivy/domain/usecase/SyncExchangeRatesUseCaseTest.kt b/shared/domain/src/androidTest/java/com/ivy/domain/usecase/SyncExchangeRatesUseCaseTest.kt index 5ecce51e7b..d1dee4af3b 100644 --- a/shared/domain/src/androidTest/java/com/ivy/domain/usecase/SyncExchangeRatesUseCaseTest.kt +++ b/shared/domain/src/androidTest/java/com/ivy/domain/usecase/SyncExchangeRatesUseCaseTest.kt @@ -11,7 +11,6 @@ import com.ivy.data.di.KtorClientModule import com.ivy.data.model.primitive.AssetCode import com.ivy.data.remote.impl.RemoteExchangeRatesDataSourceImpl import com.ivy.data.repository.ExchangeRatesRepository -import com.ivy.data.repository.impl.ExchangeRatesRepositoryImpl import com.ivy.data.repository.mapper.ExchangeRateMapper import com.ivy.domain.usecase.exchange.SyncExchangeRatesUseCase import io.kotest.assertions.arrow.core.shouldBeRight @@ -36,7 +35,7 @@ class SyncExchangeRatesUseCaseTest { val context = ApplicationProvider.getApplicationContext() db = Room.inMemoryDatabaseBuilder(context, IvyRoomDatabase::class.java).build() - repository = ExchangeRatesRepositoryImpl( + repository = ExchangeRatesRepository( exchangeRatesDao = db.exchangeRatesDao, writeExchangeRatesDao = db.writeExchangeRatesDao, mapper = ExchangeRateMapper(), diff --git a/shared/domain/src/test/java/com/ivy/domain/usecase/csv/ExportCsvUseCasePropertyTest.kt b/shared/domain/src/test/java/com/ivy/domain/usecase/csv/ExportCsvUseCasePropertyTest.kt index b3e810e6e7..02b4860b76 100644 --- a/shared/domain/src/test/java/com/ivy/domain/usecase/csv/ExportCsvUseCasePropertyTest.kt +++ b/shared/domain/src/test/java/com/ivy/domain/usecase/csv/ExportCsvUseCasePropertyTest.kt @@ -53,7 +53,7 @@ class ExportCsvUseCasePropertyTest { }.map { Arb.account(accountId = Some(it)).next() } - coEvery { accountRepository.findAll(any()) } returns accounts + coEvery { accountRepository.findAll() } returns accounts val categories = trns .mapNotNull(Transaction::category) .map { @@ -65,7 +65,7 @@ class ExportCsvUseCasePropertyTest { this } } - coEvery { categoryRepository.findAll(any()) } returns categories + coEvery { categoryRepository.findAll() } returns categories // when val csv = useCase.exportCsv { trns } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/Account.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/Account.kt index 6c48497d7b..98519f2f1b 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/Account.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/Account.kt @@ -11,7 +11,6 @@ import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.data.repository.CurrencyRepository -import java.time.Instant import java.util.UUID import com.ivy.data.model.Account as DomainAccount @@ -56,8 +55,6 @@ data class Account( icon = icon?.let(IconAsset::from)?.getOrNull(), includeInBalance = includeInBalance, orderNum = orderNum, - lastUpdated = Instant.now(), - removed = isDeleted, ) } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/wallet/CalcWalletBalanceAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/wallet/CalcWalletBalanceAct.kt index d7f99cebda..a2dd838204 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/wallet/CalcWalletBalanceAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/wallet/CalcWalletBalanceAct.kt @@ -18,7 +18,6 @@ import com.ivy.wallet.domain.action.exchange.ExchangeAct import com.ivy.wallet.domain.pure.data.ClosedTimeRange import com.ivy.wallet.domain.pure.exchange.ExchangeData import java.math.BigDecimal -import java.time.Instant import javax.inject.Inject class CalcWalletBalanceAct @Inject constructor( @@ -45,8 +44,6 @@ class CalcWalletBalanceAct @Inject constructor( icon = account.icon?.let { IconAsset.from(it).getOrNull() }, includeInBalance = account.includeInBalance, orderNum = account.orderNum, - lastUpdated = Instant.EPOCH, - removed = account.isDeleted ), range = range ) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/AccountCreator.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/AccountCreator.kt index 2c1320605d..51794e1d46 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/AccountCreator.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/AccountCreator.kt @@ -15,7 +15,6 @@ import com.ivy.legacy.utils.ioThread import com.ivy.wallet.domain.deprecated.logic.WalletAccountLogic import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData import com.ivy.wallet.domain.pure.util.nextOrderNum -import java.time.Instant import java.util.UUID import javax.inject.Inject import com.ivy.legacy.datamodel.Account as LegacyAccount @@ -41,8 +40,6 @@ class AccountCreator @Inject constructor( icon = data.icon?.let(IconAsset::from)?.getOrNull(), includeInBalance = data.includeBalance, orderNum = accountDao.findMaxOrderNum().nextOrderNum(), - lastUpdated = Instant.now(), - removed = false, ) }.getOrNull() ?: return@ioThread accountRepository.save(account) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/BudgetCreator.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/BudgetCreator.kt index 6dc6b65388..81e27da894 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/BudgetCreator.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/BudgetCreator.kt @@ -69,7 +69,7 @@ class BudgetCreator @Inject constructor( ) { try { ioThread { - budgetWriter.flagDeleted(budget.id) + budgetWriter.deleteById(budget.id) } onRefreshUI() diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/CategoryCreator.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/CategoryCreator.kt index d8c2c829c8..6d0b7b0eeb 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/CategoryCreator.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/CategoryCreator.kt @@ -11,7 +11,6 @@ import com.ivy.data.repository.CategoryRepository import com.ivy.legacy.utils.ioThread import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import com.ivy.wallet.domain.pure.util.nextOrderNum -import java.time.Instant import java.util.UUID import javax.inject.Inject @@ -34,8 +33,6 @@ class CategoryCreator @Inject constructor( icon = data.icon?.let(IconAsset::from)?.getOrNull(), orderNum = categoryRepository.findMaxOrderNum().nextOrderNum(), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, - removed = false, ) }.getOrNull() diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanCreator.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanCreator.kt index 39e56ea115..35e0ddb30a 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanCreator.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanCreator.kt @@ -78,7 +78,7 @@ class LoanCreator @Inject constructor( ) { try { ioThread { - loanWriter.flagDeleted(item.id) + loanWriter.deleteById(item.id) } onRefreshUI() 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 03483c535f..4504b6d4e5 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 @@ -72,7 +72,7 @@ class LoanRecordCreator @Inject constructor( ) { try { ioThread { - loanRecordWriter.flagDeleted(item.id) + loanRecordWriter.deleteById(item.id) } onRefreshUI() diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsGenerator.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsGenerator.kt index 2932f68397..d3e115daaf 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsGenerator.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsGenerator.kt @@ -19,7 +19,7 @@ class PlannedPaymentsGenerator @Inject constructor( suspend fun generate(rule: PlannedPaymentRule) { // delete all not happened transactions - transactionRepository.flagDeletedByRecurringRuleIdAndNoDateTime( + transactionRepository.deletedByRecurringRuleIdAndNoDateTime( recurringRuleId = rule.id ) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt index 46e481a831..3896cec2c2 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt @@ -189,7 +189,7 @@ class PlannedPaymentsLogic @Inject constructor( if (plannedPaymentRule != null && plannedPaymentRule.oneTime) { // delete paid oneTime planned payment rules - plannedPaymentRuleWriter.flagDeleted(plannedPaymentRule.id) + plannedPaymentRuleWriter.deleteById(plannedPaymentRule.id) } } @@ -221,7 +221,7 @@ class PlannedPaymentsLogic @Inject constructor( if (plannedPaymentRule != null && plannedPaymentRule.oneTime) { // delete paid oneTime planned payment rules - plannedPaymentRuleWriter.flagDeleted(plannedPaymentRule.id) + plannedPaymentRuleWriter.deleteById(plannedPaymentRule.id) } } @@ -265,7 +265,7 @@ class PlannedPaymentsLogic @Inject constructor( plannedPaymentRules.forEach { plannedPaymentRule -> if (plannedPaymentRule != null && plannedPaymentRule.oneTime) { // delete paid oneTime planned payment rules - plannedPaymentRuleWriter.flagDeleted(plannedPaymentRule.id) + plannedPaymentRuleWriter.deleteById(plannedPaymentRule.id) } } } @@ -317,7 +317,7 @@ class PlannedPaymentsLogic @Inject constructor( plannedPaymentRules.forEach { plannedPaymentRule -> if (plannedPaymentRule != null && plannedPaymentRule.oneTime) { // delete paid oneTime planned payment rules - plannedPaymentRuleWriter.flagDeleted(plannedPaymentRule.id) + plannedPaymentRuleWriter.deleteById(plannedPaymentRule.id) } } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PreloadDataLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PreloadDataLogic.kt index a06694c5f7..16874a9ca2 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PreloadDataLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PreloadDataLogic.kt @@ -43,7 +43,6 @@ import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.time.Instant import java.util.UUID import javax.inject.Inject @@ -187,8 +186,6 @@ class PreloadDataLogic @Inject constructor( icon = data.icon?.let(IconAsset::from)?.getOrNull(), orderNum = categoryOrderNum++, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, - removed = false, ) }.getOrNull() diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt index 30e73b76e0..42253a9731 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt @@ -35,7 +35,6 @@ import com.opencsv.validators.RowValidator import kotlinx.collections.immutable.persistentListOf import timber.log.Timber import java.io.StringReader -import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -526,8 +525,6 @@ class CSVImporter @Inject constructor( icon = icon?.let(IconAsset::from)?.getOrNull(), orderNum = orderNum ?: categoryRepository.findMaxOrderNum().nextOrderNum(), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, - removed = false, ) }.getOrNull() diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt index 51bde7e71a..3da5b43114 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt @@ -35,7 +35,6 @@ import com.ivy.wallet.domain.deprecated.logic.currency.ExchangeRatesLogic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.time.Instant import java.time.LocalDateTime import java.util.Locale import java.util.UUID @@ -235,9 +234,7 @@ class LoanTransactionsCore @Inject constructor( color = ColorInt(IVY_COLOR_PICKER_COLORS_FREE[DEFAULT_COLOR_INDEX].toArgb()), icon = IconAsset.unsafe("loan"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) } else { null diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/ChooseCategoryModal.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/ChooseCategoryModal.kt index 946d10b720..9b6a7bbf5b 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/ChooseCategoryModal.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/ChooseCategoryModal.kt @@ -53,7 +53,6 @@ import com.ivy.wallet.ui.theme.modal.IvyModal import com.ivy.wallet.ui.theme.modal.ModalSkip import com.ivy.wallet.ui.theme.modal.ModalTitle import com.ivy.wallet.ui.theme.toComposeColor -import java.time.Instant import java.util.UUID @Deprecated("Old design system. Use `:ivy-design` and Material3") @@ -296,27 +295,21 @@ private fun PreviewChooseCategoryModal() { color = ColorInt(Ivy.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Second"), color = ColorInt(Orange.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), Category( name = NotBlankTrimmedString.unsafe("Third"), color = ColorInt(Red.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ), ) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt index 3bd3939749..ccf1a3d4de 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt @@ -81,7 +81,6 @@ import com.ivy.wallet.ui.theme.toComposeColor import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import java.time.Instant import java.time.LocalDateTime import java.util.UUID @@ -401,7 +400,8 @@ private fun TransactionBadge( .background(backgroundColor, UI.shapes.rFull) .clickable { onClick() - }.padding(end = 10.dp), + } + .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically ) { SpacerHor(width = 8.dp) @@ -594,9 +594,7 @@ private fun PreviewUpcomingExpense() { color = ColorInt(Blue.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { @@ -634,9 +632,7 @@ private fun PreviewUpcomingExpenseBadgeSecondRow() { color = ColorInt(Blue.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { @@ -674,9 +670,7 @@ private fun PreviewOverdueExpense() { color = ColorInt(Green.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { @@ -714,9 +708,7 @@ private fun PreviewNormalExpense() { color = ColorInt(Orange.toArgb()), icon = IconAsset.unsafe("groceries"), id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { @@ -753,9 +745,7 @@ private fun PreviewIncome() { color = ColorInt(GreenDark.toArgb()), icon = null, id = CategoryId(UUID.randomUUID()), - lastUpdated = Instant.EPOCH, orderNum = 0.0, - removed = false, ) item { From ddf583f988bbfb8d4965e73d3cc4996987476dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Bieli=C5=84ski?= <105647564+CodinGeo@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:09:13 +0200 Subject: [PATCH 02/16] PR template readability and standardization fixes (#3319) * PR template readability and standardization enhancements * PR template check styling changed * PR template table change * PR template - little fixes * PR template - removed unnecessary 'please' * PR template small fixes --- .github/PULL_REQUEST_TEMPLATE.md | 69 ++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d02e858671..128b62d122 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,52 +1,71 @@ -## Pull Request (PR) Checklist +## Pull request (PR) checklist + Please check if your pull request fulfills the following requirements: + + + - [ ] The PR is submitted to the `main` branch. - [ ] I've read the [Contribution Guidelines](https://github.com/Ivy-Apps/ivy-wallet/blob/main/CONTRIBUTING.md) and my PR doesn't break the rules. - [ ] I've read and understand the [Developer Guidelines](https://github.com/Ivy-Apps/ivy-wallet/blob/main/docs/Guidelines.md). - [ ] I confirm that I've run the code locally and everything works as expected. -- [ ] 🎬 I've attached a **screen recoding** of the changes. - [ ] My PR includes only the necessary changes to fix the issue (i.e., no unnecessary files or lines of code are changed). +- [ ] 🎬 I've attached a **screen recording** of using the new code to the next paragraph (if applicable). + + +## Screen recording of interacting with your changes: + + + -> Tip: drag & drop the video to the PR description. ## What's changed? - -Describe with a few bullets **what's new:** -- a -- b -- c -> 💡 Tip: Please, attach screenshots and screen recordings. It helps a lot! +- ... + + +- ... + -## Risk Factors +## Risk factors **What may go wrong if we merge your PR?** -- a -- b +- ... +- ... -**In what cases your code won't work?** +**In what cases won't your code work?** -- a -- b +- ... +- ... -## Does this PR closes any GitHub Issues? -Check **[Ivy Wallet Issues](https://github.com/Ivy-Apps/ivy-wallet/issues)**. +## Does this PR close any GitHub issues? -- Closes #ISSUE_NUMBER +- Closes #{ISSUE_NUMBER} + + + + + -> **!Note:** Do **not** link the PR number. Link the number/id of the GitHub Issue from [issues](https://github.com/Ivy-Apps/ivy-wallet/issues). ## Troubleshooting CI failures ❌ -GitHub Actions failing? Read our [CI Troubleshooting guide](https://github.com/Ivy-Apps/ivy-wallet/blob/main/docs/CI-Troubleshooting.md). +Pull request checks failing? Read our [CI Troubleshooting guide](https://github.com/Ivy-Apps/ivy-wallet/blob/main/docs/CI-Troubleshooting.md). \ No newline at end of file From 52e4e8af4cc226ce51e7db9744086577135772be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:17:31 +0300 Subject: [PATCH 03/16] Automatic release: v2024.07.10 (187) (#3323) Co-authored-by: GitHub Actions --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9cb14f7ea0..ec43a1ba82 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +17,8 @@ paparazzi = "1.3.3" # Android min-sdk = "28" compile-sdk = "34" -version-name = "2024.07.07" -version-code = "186" +version-name = "2024.07.10" +version-code = "187" jvm-target = "17" From 6cc2f095e364eb7a49a3b255c6e410dad00b3cef Mon Sep 17 00:00:00 2001 From: Suyash Mittal Date: Thu, 11 Jul 2024 01:10:41 +0530 Subject: [PATCH 04/16] Different decimal place for crypto and fiat currency (#3324) * WIP: disabled firebase crashlytics * fixed decimal place for crypto * simplify get decimal place logic --- .../com/ivy/legacy/domain/data/IvyCurrency.kt | 6 +++++ .../components/CustomExchangeRateCard.kt | 26 +++++++++++++++---- .../ui/component/edit/core/EditBottomSheet.kt | 11 +++++++- .../component/transaction/TransactionCard.kt | 6 ++++- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt index f00b38ea56..ae22115a38 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt @@ -11,6 +11,9 @@ data class IvyCurrency( val isCrypto: Boolean ) { companion object { + private const val CRYPTO_DECIMAL = 18 + private const val FIAT_DECIMAL = 2 + private val CRYPTO = setOf( IvyCurrency( code = "BTC", @@ -208,6 +211,9 @@ data class IvyCurrency( fun getDefault(): IvyCurrency = IvyCurrency( fiatCurrency = getDefaultFIATCurrency() ) + + fun getDecimalPlaces(assetCode: String): Int = + if (fromCode(assetCode) in CRYPTO) CRYPTO_DECIMAL else FIAT_DECIMAL } constructor(fiatCurrency: Currency) : this( diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/CustomExchangeRateCard.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/CustomExchangeRateCard.kt index 6a0daa5762..c9e6c5bedc 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/CustomExchangeRateCard.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/CustomExchangeRateCard.kt @@ -24,18 +24,19 @@ import com.ivy.design.l0_system.style import com.ivy.legacy.IvyWalletComponentPreview import com.ivy.legacy.utils.format import com.ivy.ui.R +import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.ui.theme.Orange @Deprecated("Old design system. Use `:ivy-design` and Material3") @Composable fun CustomExchangeRateCard( - modifier: Modifier = Modifier, - title: String = stringResource(R.string.exchange_rate), fromCurrencyCode: String, toCurrencyCode: String, exchangeRate: Double, + modifier: Modifier = Modifier, + title: String = stringResource(R.string.exchange_rate), onRefresh: () -> Unit = {}, - onClick: () -> Unit + onClick: () -> Unit, ) { Row( modifier = modifier @@ -80,14 +81,29 @@ fun CustomExchangeRateCard( ) IvyIcon(icon = R.drawable.ic_arrow_right, tint = Orange) Text( - text = "$toCurrencyCode \t\t:\t\t", + text = toCurrencyCode, + style = UI.typo.nB2.style( + fontWeight = FontWeight.ExtraBold, + color = Orange + ) + ) + } + + Spacer(Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "1", style = UI.typo.nB2.style( fontWeight = FontWeight.ExtraBold, color = Orange ) ) + IvyIcon(icon = R.drawable.ic_arrow_right, tint = Orange) Text( - text = exchangeRate.format(4), + text = exchangeRate.format(IvyCurrency.getDecimalPlaces(toCurrencyCode)), style = UI.typo.nB2.style( fontWeight = FontWeight.ExtraBold, color = Orange diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/EditBottomSheet.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/EditBottomSheet.kt index 36a56c8496..e75d0483f9 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/EditBottomSheet.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/EditBottomSheet.kt @@ -67,6 +67,7 @@ import com.ivy.base.model.TransactionType import com.ivy.legacy.utils.rememberInteractionSource import com.ivy.legacy.utils.rememberSwipeListenerState import com.ivy.ui.R +import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.ui.theme.Gradient import com.ivy.wallet.ui.theme.Green import com.ivy.wallet.ui.theme.GreenDark @@ -150,7 +151,15 @@ fun BoxWithConstraintsScope.EditBottomSheet( val showConvertedAmountText by remember(convertedAmount) { if (type == TransactionType.TRANSFER && convertedAmount != null && convertedAmountCurrencyCode != null) { - mutableStateOf("${convertedAmount.format(2)} $convertedAmountCurrencyCode") + mutableStateOf( + "${ + convertedAmount.format( + IvyCurrency.getDecimalPlaces( + convertedAmountCurrencyCode + ) + ) + } $convertedAmountCurrencyCode" + ) } else { mutableStateOf(null) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt index ccf1a3d4de..6da77bf0a6 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/transaction/TransactionCard.kt @@ -58,6 +58,7 @@ import com.ivy.navigation.Navigation import com.ivy.navigation.TransactionsScreen import com.ivy.navigation.navigation import com.ivy.ui.R +import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.ui.theme.Blue import com.ivy.wallet.ui.theme.Gradient import com.ivy.wallet.ui.theme.GradientGreen @@ -196,7 +197,10 @@ fun TransactionCard( if (transaction.type == TransactionType.TRANSFER && toAccountCurrency != transactionCurrency) { Text( modifier = Modifier.padding(start = 68.dp), - text = "${transaction.toAmount.toDouble().format(2)} $toAccountCurrency", + text = "${ + transaction.toAmount.toDouble() + .format(IvyCurrency.getDecimalPlaces(toAccountCurrency)) + } $toAccountCurrency", style = UI.typo.nB2.style( color = Gray, fontWeight = FontWeight.Normal From c798c7fce85dd93880b2595a0ee15729f1d2c586 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Thu, 11 Jul 2024 00:18:55 +0300 Subject: [PATCH 05/16] Remove unnecessary permission (#3325) * Remove unnecessary permission * Fix release announcement --- .github/workflows/internal_release.yml | 2 +- app/src/main/AndroidManifest.xml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index 7e969ea53a..98173b2737 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -183,7 +183,7 @@ jobs: run: | RELEASE_TAG=${{ env.TAG_NAME }} RELEASE_URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" - CAPTION=$'🚀 Ivy Wallet '"$RELEASE_TAG"$' release\n\nCheck it out here: '"$RELEASE_URL"' \n\n🚢 Please, test the release candidate before we ship it.' + CAPTION=$'🚀 Ivy Wallet '"$RELEASE_TAG"$' release\n\nCheck it out here: '"$RELEASE_URL"' \n\n 🚢 Please, test the release candidate before we ship it.' curl -F "chat_id=-1001647280565" \ -F "document=@app/build/outputs/apk/demo/app-demo.apk" \ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 838988d38f..6925031e68 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ - - \ No newline at end of file + From b21d76a6e799986707981cb6d2c191d74953969f Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Thu, 11 Jul 2024 00:22:31 +0300 Subject: [PATCH 06/16] Update default release notes --- fastlane/metadata/android/en-GB/changelogs/default.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-GB/changelogs/default.txt b/fastlane/metadata/android/en-GB/changelogs/default.txt index 75a94b0a79..6e43f09d83 100644 --- a/fastlane/metadata/android/en-GB/changelogs/default.txt +++ b/fastlane/metadata/android/en-GB/changelogs/default.txt @@ -1 +1 @@ -Ivy Wallet internal testing release, please refer to (https://github.com/Ivy-Apps/ivy-wallet/releases) for changelog. \ No newline at end of file +Ivy Wallet periodic release, refer to (https://github.com/Ivy-Apps/ivy-wallet/releases) for changelog. If you find any issues, please report them in our GitHub repository. From 247390da466b765967fce95b9c288ed997aa3107 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 00:23:31 +0300 Subject: [PATCH 07/16] Automatic release: v2024.07.10 (188) (#3326) Co-authored-by: GitHub Actions --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec43a1ba82..1eba21fac1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ paparazzi = "1.3.3" min-sdk = "28" compile-sdk = "34" version-name = "2024.07.10" -version-code = "187" +version-code = "188" jvm-target = "17" From 288e83db41cb7ed157b227fbbe718d783bf67674 Mon Sep 17 00:00:00 2001 From: Vivek <55149882+vk92257@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:15:39 +0530 Subject: [PATCH 08/16] Added the tonchain currency (#3329) --- .../src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt index ae22115a38..19a57c24f1 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/data/IvyCurrency.kt @@ -175,6 +175,11 @@ data class IvyCurrency( name = "Tron", isCrypto = true ), + IvyCurrency( + code = "TON", + name = "Tonchain", + isCrypto = true + ), ) fun getAvailable(): List { From 66680f3a6e6a7d2ca9a1db3dcc50a68633a202f6 Mon Sep 17 00:00:00 2001 From: Vivek <55149882+vk92257@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:09:28 +0530 Subject: [PATCH 09/16] Remove the library that causing 3328 (#3330) * Added the tonchain currency * Remove the library that causing the foreground permission issue * Remove the library that causing the foreground permission issue * Remove the commented the library --- gradle/libs.versions.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1eba21fac1..5a6c05b04e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -105,7 +105,6 @@ hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", # Google google-services-plugin = { module = "com.google.gms:google-services", version = "4.4.2" } google-playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "21.2.0" } -google-play-asset = { module = "com.google.android.play:asset-delivery", version = "2.2.2" } google-play-update = { module = "com.google.android.play:app-update", version = "2.1.0" } google-play-services = { module = "com.google.android.gms:play-services-tasks", version = "18.2.0" } google-play-review = { module = "com.google.android.play:review-ktx", version = "2.0.1" } @@ -211,7 +210,6 @@ glance = [ ] google = [ "google-playservices-auth", - "google-play-asset", "google-play-update", "google-play-services", "google-play-review" From 899439f8e68438ee35f6c9a24eda1984ea617fab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:41:39 +0300 Subject: [PATCH 10/16] Automatic release: v2024.07.11 (189) (#3331) Co-authored-by: GitHub Actions --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a6c05b04e..fb16ce5376 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +17,8 @@ paparazzi = "1.3.3" # Android min-sdk = "28" compile-sdk = "34" -version-name = "2024.07.10" -version-code = "188" +version-name = "2024.07.11" +version-code = "189" jvm-target = "17" From 2c38ca57a3a3abe77f6ca4b0e906a3d3f569a3c2 Mon Sep 17 00:00:00 2001 From: Suyash Mittal Date: Fri, 12 Jul 2024 00:23:24 +0530 Subject: [PATCH 11/16] fixes decimal place in amount input sheet (#3334) * WIP: disabled firebase crashlytics * fixed decimal place for crypto * simplify get decimal place logic * dynamic decimal place in amount input sheet * dynamic decimal place in amount input sheet --- .../main/java/com/ivy/transaction/EditTransactionScreen.kt | 5 ++++- .../com/ivy/legacy/legacy/ui/theme/modal/edit/AmountModal.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt index 7bb729036a..0370fbd973 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt @@ -59,6 +59,7 @@ import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import com.ivy.wallet.ui.edit.core.Category import com.ivy.legacy.ui.component.edit.core.Description +import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.ui.edit.core.DueDate import com.ivy.wallet.ui.edit.core.EditBottomSheet import com.ivy.wallet.ui.edit.core.Title @@ -604,7 +605,9 @@ private fun BoxWithConstraintsScope.UI( currency = "", initialAmount = customExchangeRateState.exchangeRate, dismiss = { exchangeRateAmountModalShown = false }, - decimalCountMax = 4, + decimalCountMax = IvyCurrency.getDecimalPlaces( + customExchangeRateState.toCurrencyCode ?: baseCurrency + ), onAmountChanged = { onExchangeRateChange(it) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/AmountModal.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/AmountModal.kt index d76289d210..bdbfc5951e 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/AmountModal.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/edit/AmountModal.kt @@ -200,7 +200,7 @@ fun AmountCurrency( Text( text = amount.ifBlank { "0" }, - style = UI.typo.nH1.style( + style = UI.typo.nH2.style( fontWeight = FontWeight.Bold, color = UI.colors.pureInverse ) From 24555e33e49e95d3f30f9d75d890e76a9222d35f Mon Sep 17 00:00:00 2001 From: Ahsan Ishaq Date: Sat, 13 Jul 2024 01:51:32 +0500 Subject: [PATCH 12/16] Removed all code related to sign in through google from app. (#3333) * Removed all code related to sign in through google from gradle, imports and files. * Format code according to detekt tasks. * Added annotations to handle detekt errors. * Correct way to pass multiple parameter in Suppress annotation. * Removed some extra libraries and code. --------- Co-authored-by: DESKTOP-NEFNNLO\AHSAN ISHAQ Co-authored-by: Ahsan Ishaq --- app/build.gradle.kts | 1 + app/src/main/java/com/ivy/IvyNavGraph.kt | 1 + .../main/java/com/ivy/wallet/RootActivity.kt | 61 +++---------------- .../main/java/com/ivy/wallet/RootViewModel.kt | 4 ++ .../wallet/migrations/MigrationsManager.kt | 1 + .../wallet/ui/applocked/AppLockedScreen.kt | 4 ++ config/detekt/baseline.yml | 1 - config/detekt/config.yml | 6 -- gradle/libs.versions.toml | 2 - .../com/ivy/onboarding/OnboardingEvent.kt | 1 - .../onboarding/steps/OnboardingSplashLogin.kt | 36 ----------- .../viewmodel/OnboardingViewModel.kt | 29 --------- 12 files changed, 21 insertions(+), 126 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c1812f455..a63a8474b5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { id("com.google.devtools.ksp") id("com.google.gms.google-services") id("com.google.firebase.crashlytics") + id("io.gitlab.arturbosch.detekt") } android { diff --git a/app/src/main/java/com/ivy/IvyNavGraph.kt b/app/src/main/java/com/ivy/IvyNavGraph.kt index c28e9b988a..f48f234c1f 100644 --- a/app/src/main/java/com/ivy/IvyNavGraph.kt +++ b/app/src/main/java/com/ivy/IvyNavGraph.kt @@ -55,6 +55,7 @@ import com.ivy.transactions.TransactionsScreen @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable +@Suppress("CyclomaticComplexMethod", "FunctionNaming") fun BoxWithConstraintsScope.IvyNavGraph(screen: Screen?) { when (screen) { null -> { diff --git a/app/src/main/java/com/ivy/wallet/RootActivity.kt b/app/src/main/java/com/ivy/wallet/RootActivity.kt index 66a280723d..2369ebb5a2 100644 --- a/app/src/main/java/com/ivy/wallet/RootActivity.kt +++ b/app/src/main/java/com/ivy/wallet/RootActivity.kt @@ -26,12 +26,6 @@ import androidx.compose.runtime.getValue import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount -import com.google.android.gms.auth.api.signin.GoogleSignInClient -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.tasks.Task import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat @@ -56,12 +50,12 @@ import com.ivy.widget.balance.WalletBalanceWidgetReceiver import com.ivy.widget.transaction.AddTransactionWidget import com.ivy.widget.transaction.AddTransactionWidgetCompact import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber import java.time.LocalDate import java.time.LocalTime import javax.inject.Inject @AndroidEntryPoint +@Suppress("TooManyFunctions") class RootActivity : AppCompatActivity(), RootScreen { @Inject lateinit var ivyContext: IvyWalletCtx @@ -72,9 +66,6 @@ class RootActivity : AppCompatActivity(), RootScreen { @Inject lateinit var customerJourneyLogic: CustomerJourneyCardsProvider - private lateinit var googleSignInLauncher: ActivityResultLauncher - private lateinit var onGoogleSignInIdTokenResult: (idToken: String?) -> Unit - private lateinit var createFileLauncher: ActivityResultLauncher private lateinit var onFileCreated: (fileUri: Uri) -> Unit @@ -154,9 +145,9 @@ class RootActivity : AppCompatActivity(), RootScreen { private fun setupDatePicker() { ivyContext.onShowDatePicker = { minDate, - maxDate, - initialDate, - onDatePicked -> + maxDate, + initialDate, + onDatePicked -> val datePicker = MaterialDatePicker.Builder.datePicker() .setSelection( @@ -203,53 +194,19 @@ class RootActivity : AppCompatActivity(), RootScreen { .build() picker.show(supportFragmentManager, "timePicker") picker.addOnPositiveButtonClickListener { - onTimePicked(LocalTime.of(picker.hour, picker.minute).convertLocalToUTC().withSecond(0)) + onTimePicked( + LocalTime.of(picker.hour, picker.minute).convertLocalToUTC().withSecond(0) + ) } } } private fun setupActivityForResultLaunchers() { - googleSignInLauncher() - createFileLauncher() openFileLauncher() } - private fun googleSignInLauncher() { - googleSignInLauncher = activityForResultLauncher( - createIntent = { _, client -> - client.signInIntent - } - ) { _, intent -> - try { - val task: Task = - GoogleSignIn.getSignedInAccountFromIntent(intent) - val account: GoogleSignInAccount = task.getResult(ApiException::class.java) - val idToken = account.idToken - Timber.d("idToken = $idToken") - - onGoogleSignInIdTokenResult(idToken) - } catch (e: ApiException) { - e.sendToCrashlytics("GOOGLE_SIGN_IN - registerGoogleSignInContract(): ApiException") - e.printStackTrace() - onGoogleSignInIdTokenResult(null) - } - } - - ivyContext.googleSignIn = { idTokenResult: (String?) -> Unit -> - onGoogleSignInIdTokenResult = idTokenResult - - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestEmail() - .requestProfile() - .requestIdToken("364763737033-t1d2qe7s0s8597k7anu3sb2nq79ot5tp.apps.googleusercontent.com") - .build() - val googleSignInClient = GoogleSignIn.getClient(this, gso) - googleSignInLauncher.launch(googleSignInClient) - } - } - private fun createFileLauncher() { createFileLauncher = activityForResultLauncher( createIntent = { _, fileName -> @@ -344,7 +301,7 @@ class RootActivity : AppCompatActivity(), RootScreen { ) .setAllowedAuthenticators( BiometricManager.Authenticators.BIOMETRIC_WEAK or - BiometricManager.Authenticators.DEVICE_CREDENTIAL + BiometricManager.Authenticators.DEVICE_CREDENTIAL ) .setConfirmationRequired(false) .build() @@ -362,6 +319,7 @@ class RootActivity : AppCompatActivity(), RootScreen { } } + @Suppress("TooGenericExceptionCaught", "PrintStackTrace") override fun openUrlInBrowser(url: String) { try { val browserIntent = Intent(Intent.ACTION_VIEW) @@ -390,6 +348,7 @@ class RootActivity : AppCompatActivity(), RootScreen { startActivity(share) } + @Suppress("SwallowedException") override fun openGooglePlayAppPage(appId: String) { try { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appId"))) diff --git a/app/src/main/java/com/ivy/wallet/RootViewModel.kt b/app/src/main/java/com/ivy/wallet/RootViewModel.kt index 4eba48176c..b00b69c4c6 100644 --- a/app/src/main/java/com/ivy/wallet/RootViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/RootViewModel.kt @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicLong import javax.inject.Inject @HiltViewModel +@Suppress("LongParameterList", "TooManyFunctions") class RootViewModel @Inject constructor( private val ivyContext: IvyWalletCtx, private val nav: Navigation, @@ -103,6 +104,7 @@ class RootViewModel @Inject constructor( } } + @Suppress("SwallowedException") private fun handleSpecialStart(intent: Intent): Boolean { val addTrnType: TransactionType? = try { intent.getSerializableExtra(EXTRA_ADD_TRANSACTION_TYPE) as? TransactionType @@ -125,6 +127,7 @@ class RootViewModel @Inject constructor( return false } + @Suppress("EmptyFunctionBlock") fun handleBiometricAuthResult( onAuthSuccess: () -> Unit = {} ): BiometricPrompt.AuthenticationCallback { @@ -169,6 +172,7 @@ class RootViewModel @Inject constructor( private val userInactiveTime = AtomicLong(0) private var userInactiveJob: Job? = null + @Suppress("MagicNumber") fun startUserInactiveTimeCounter() { if (userInactiveJob != null && userInactiveJob!!.isActive) return diff --git a/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt b/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt index 7ce3dc926b..7bb3df5fe3 100644 --- a/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt +++ b/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt @@ -25,6 +25,7 @@ class MigrationsManager @Inject constructor( ) } + @Suppress("TooGenericExceptionCaught", "PrintStackTrace", "MagicNumber") suspend fun executeMigrations() { delay(2_000L) // to not the make the app start slower diff --git a/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt b/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt index a7a6fecc66..4a7c99e43c 100644 --- a/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt @@ -1,5 +1,6 @@ package com.ivy.wallet.ui.applocked +import android.annotation.SuppressLint import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -37,7 +38,9 @@ import com.ivy.wallet.ui.theme.Gray import com.ivy.wallet.ui.theme.White import com.ivy.wallet.ui.theme.components.IvyButton +@SuppressLint("ComposeModifierMissing") @Composable +@Suppress("LongMethod", "FunctionNaming") fun BoxWithConstraintsScope.AppLockedScreen( onShowOSBiometricsModal: () -> Unit, onContinueWithoutAuthentication: () -> Unit @@ -135,6 +138,7 @@ private fun osAuthentication( @Preview @Composable +@Suppress("FunctionNaming", "UnusedPrivateMember") private fun Preview_Locked() { IvyWalletPreview { AppLockedScreen( diff --git a/config/detekt/baseline.yml b/config/detekt/baseline.yml index 81f5b18692..960eaceff6 100644 --- a/config/detekt/baseline.yml +++ b/config/detekt/baseline.yml @@ -10637,7 +10637,6 @@ UnusedParameter:Title.kt$initialTransactionId: UUID? UnusedPrivateMember:EditTransactionViewModel.kt$EditTransactionViewModel$private suspend fun transferToAmount( amount: Double ): Double? UnusedPrivateMember:Migration109to110_PlannedPayments.kt$Migration109to110_PlannedPayments$private fun SupportSQLiteDatabase.addSyncColumns(tableName: String) - UnusedPrivateMember:OnboardingSplashLogin.kt$@Composable private fun LoginWithGoogleExplanation() UnusedPrivateMember:SettingsScreen.kt$@Composable private fun AccountCardButton( @DrawableRes icon: Int, text: String, onClick: () -> Unit ) UnusedPrivateProperty:AccountModal.kt$val context = LocalContext.current UnusedPrivateProperty:DateExt.kt$val seconds = TimeUnit.MILLISECONDS.toSeconds(timeLeftAfterCalculations) diff --git a/config/detekt/config.yml b/config/detekt/config.yml index db972225a0..634a2b0993 100644 --- a/config/detekt/config.yml +++ b/config/detekt/config.yml @@ -509,12 +509,6 @@ style: active: false singleLine: 'never' multiLine: 'always' - # OptionalWhenBraces is the deprecated rule, use OptionalWhenBraces instead. - # Use it now because BracesOnWhenStatements doesn't work as expected - # https://github.com/detekt/detekt/issues/6377 - # Delete OptionalWhenBraces rule config after detekt#6377 fix - OptionalWhenBraces: - active: false BracesOnWhenStatements: active: false singleLine: 'necessary' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb16ce5376..a53ecdcf22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -104,7 +104,6 @@ hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", # Google google-services-plugin = { module = "com.google.gms:google-services", version = "4.4.2" } -google-playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "21.2.0" } google-play-update = { module = "com.google.android.play:app-update", version = "2.1.0" } google-play-services = { module = "com.google.android.gms:play-services-tasks", version = "18.2.0" } google-play-review = { module = "com.google.android.play:review-ktx", version = "2.0.1" } @@ -209,7 +208,6 @@ glance = [ "glance-material3" ] google = [ - "google-playservices-auth", "google-play-update", "google-play-services", "google-play-review" diff --git a/screen/onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt b/screen/onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt index 8d09a30b50..437bee2851 100644 --- a/screen/onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt +++ b/screen/onboarding/src/main/java/com/ivy/onboarding/OnboardingEvent.kt @@ -8,7 +8,6 @@ import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData sealed interface OnboardingEvent { - data object LoginWithGoogle : OnboardingEvent data object LoginOfflineAccount : OnboardingEvent data object StartImport : OnboardingEvent data object ImportSkip : OnboardingEvent diff --git a/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt b/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt index 291867a8dc..6a00de6380 100644 --- a/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt +++ b/screen/onboarding/src/main/java/com/ivy/onboarding/steps/OnboardingSplashLogin.kt @@ -292,42 +292,6 @@ private fun LoginSection( } } -@Composable -private fun LoginWithGoogleExplanation() { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) - - IvyIcon( - icon = R.drawable.ic_secure, - tint = Green - ) - - Spacer(Modifier.width(4.dp)) - - Column { - Text( - text = stringResource(R.string.sync_data_ivy_cloud), - style = UI.typo.c.style( - color = Green, - fontWeight = FontWeight.ExtraBold - ) - ) - - Spacer(Modifier.height(2.dp)) - - Text( - text = stringResource(R.string.data_integrity_protection_warning), - style = UI.typo.c.style( - color = UI.colors.pureInverse, - fontWeight = FontWeight.Medium - ) - ) - } - } -} - @Composable private fun LocalAccountExplanation() { Text( diff --git a/screen/onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt b/screen/onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt index b8f659c8ef..606197f92c 100644 --- a/screen/onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt +++ b/screen/onboarding/src/main/java/com/ivy/onboarding/viewmodel/OnboardingViewModel.kt @@ -12,7 +12,6 @@ import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteSettingsDao import com.ivy.ui.ComposeViewModel import com.ivy.domain.usecase.exchange.SyncExchangeRatesUseCase -import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.LogoutLogic import com.ivy.data.model.Category import com.ivy.data.repository.CategoryRepository @@ -22,7 +21,6 @@ import com.ivy.legacy.datamodel.Settings import com.ivy.legacy.domain.deprecated.logic.AccountCreator import com.ivy.legacy.utils.OpResult import com.ivy.legacy.utils.ioThread -import com.ivy.legacy.utils.sendToCrashlytics import com.ivy.navigation.Navigation import com.ivy.navigation.OnboardingScreen import com.ivy.onboarding.OnboardingDetailState @@ -40,13 +38,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Stable @HiltViewModel class OnboardingViewModel @Inject constructor( - private val ivyContext: IvyWalletCtx, private val nav: Navigation, private val accountDao: AccountDao, private val settingsDao: SettingsDao, @@ -150,7 +146,6 @@ class OnboardingViewModel @Inject constructor( is OnboardingEvent.ImportFinished -> importFinished(event.success) OnboardingEvent.ImportSkip -> importSkip() OnboardingEvent.LoginOfflineAccount -> loginOfflineAccount() - OnboardingEvent.LoginWithGoogle -> loginWithGoogle() OnboardingEvent.OnAddAccountsDone -> onAddAccountsDone() OnboardingEvent.OnAddAccountsSkip -> onAddAccountsSkip() OnboardingEvent.OnAddCategoriesDone -> onAddCategoriesDone() @@ -162,30 +157,6 @@ class OnboardingViewModel @Inject constructor( } } - // Step 1 --------------------------------------------------------------------------------------- - private suspend fun loginWithGoogle() { - ivyContext.googleSignIn { idToken -> - if (idToken != null) { - _opGoogleSignIn.value = OpResult.loading() - viewModelScope.launch { - try { - router.googleLoginNext() - _opGoogleSignIn.value = null // reset login with Google operation state - } catch (e: Exception) { - e.sendToCrashlytics("GOOGLE_SIGN_IN ERROR: generic exception when logging with GOOGLE") - e.printStackTrace() - Timber.e("Login with Google failed on Ivy server - ${e.message}") - _opGoogleSignIn.value = OpResult.failure(e) - } - } - } else { - sendToCrashlytics("GOOGLE_SIGN_IN ERROR: idToken is null!!") - Timber.e("Login with Google failed while getting idToken") - _opGoogleSignIn.value = OpResult.faliure("Login with Google failed, try again.") - } - } - } - private suspend fun loginOfflineAccount() { router.offlineAccountNext() } From 77c1df18bf7caa13abcfd6faac282c76eef20b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Bieli=C5=84ski?= <105647564+CodinGeo@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:53:30 +0200 Subject: [PATCH 13/16] PR template - removed 'main' branch checkbox, in code view: deleted blank lines, clarified comments (#3336) --- .github/PULL_REQUEST_TEMPLATE.md | 48 ++++++-------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 128b62d122..25b5a232c1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,71 +1,39 @@ ## Pull request (PR) checklist - Please check if your pull request fulfills the following requirements: - - - -- [ ] The PR is submitted to the `main` branch. + - [ ] I've read the [Contribution Guidelines](https://github.com/Ivy-Apps/ivy-wallet/blob/main/CONTRIBUTING.md) and my PR doesn't break the rules. - [ ] I've read and understand the [Developer Guidelines](https://github.com/Ivy-Apps/ivy-wallet/blob/main/docs/Guidelines.md). - [ ] I confirm that I've run the code locally and everything works as expected. - [ ] My PR includes only the necessary changes to fix the issue (i.e., no unnecessary files or lines of code are changed). - [ ] 🎬 I've attached a **screen recording** of using the new code to the next paragraph (if applicable). - - ## Screen recording of interacting with your changes: - - - - + ## What's changed? Describe with a few bullets **what's new:** - - - I've fixed... Before|After --------|------- +---------|--------- {media}|{media} {media}|{media} ---> - - - ... - - - ... - - ## Risk factors - **What may go wrong if we merge your PR?** - - ... - ... **In what cases won't your code work?** - - ... - ... - - ## Does this PR close any GitHub issues? - - Closes #{ISSUE_NUMBER} - - - - - + + - - +💡--> ## Troubleshooting CI failures ❌ - Pull request checks failing? Read our [CI Troubleshooting guide](https://github.com/Ivy-Apps/ivy-wallet/blob/main/docs/CI-Troubleshooting.md). \ No newline at end of file From 84fff68e3e100280d3888c4dbeb621265b44f73e Mon Sep 17 00:00:00 2001 From: Suyash Mittal Date: Sat, 13 Jul 2024 14:10:25 +0530 Subject: [PATCH 14/16] Fix issue 3138 (#3337) * added language change option for android 13+ * fixed detekt issues * removed :app dependency for language selection * removed unused dependency --- .../java/com/ivy/settings/SettingsEvent.kt | 1 + .../java/com/ivy/settings/SettingsScreen.kt | 24 +++++++++++++++-- .../java/com/ivy/settings/SettingsState.kt | 3 ++- .../com/ivy/settings/SettingsViewModel.kt | 26 +++++++++++++++++-- .../core/src/main/res/values-ar/strings.xml | 1 + .../core/src/main/res/values-bg/strings.xml | 1 + .../core/src/main/res/values-de/strings.xml | 1 + .../core/src/main/res/values-es/strings.xml | 1 + .../core/src/main/res/values-hi/strings.xml | 1 + .../core/src/main/res/values-id/strings.xml | 1 + .../core/src/main/res/values-in/strings.xml | 1 + .../core/src/main/res/values-it/strings.xml | 1 + .../core/src/main/res/values-kn/strings.xml | 1 + .../core/src/main/res/values-mn/strings.xml | 1 + .../core/src/main/res/values-pl/strings.xml | 1 + .../src/main/res/values-pt-rBR/strings.xml | 1 + .../core/src/main/res/values-ru/strings.xml | 1 + .../core/src/main/res/values-vi/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + .../ui/core/src/main/res/values/strings.xml | 1 + 21 files changed, 66 insertions(+), 5 deletions(-) diff --git a/screen/settings/src/main/java/com/ivy/settings/SettingsEvent.kt b/screen/settings/src/main/java/com/ivy/settings/SettingsEvent.kt index dbac2c09fe..daee9fa951 100644 --- a/screen/settings/src/main/java/com/ivy/settings/SettingsEvent.kt +++ b/screen/settings/src/main/java/com/ivy/settings/SettingsEvent.kt @@ -18,4 +18,5 @@ sealed interface SettingsEvent { data class SetStartDateOfMonth(val startDate: Int) : SettingsEvent data object DeleteCloudUserData : SettingsEvent data object DeleteAllUserData : SettingsEvent + data object SwitchLanguage : SettingsEvent } diff --git a/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt b/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt index 2edac02d39..cde35564fb 100644 --- a/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt +++ b/screen/settings/src/main/java/com/ivy/settings/SettingsScreen.kt @@ -73,6 +73,7 @@ import com.ivy.wallet.ui.theme.modal.CurrencyModal import com.ivy.wallet.ui.theme.modal.DeleteModal import com.ivy.wallet.ui.theme.modal.NameModal import com.ivy.wallet.ui.theme.modal.ProgressModal +import java.util.Locale @ExperimentalFoundationApi @Composable @@ -95,6 +96,7 @@ fun BoxWithConstraintsScope.SettingsScreen() { treatTransfersAsIncomeExpense = uiState.treatTransfersAsIncomeExpense, nameLocalAccount = uiState.name, startDateOfMonth = uiState.startDateOfMonth.toInt(), + languageOptionVisible = uiState.languageOptionVisible, onSetCurrency = { viewModel.onEvent(SettingsEvent.SetCurrency(it)) }, @@ -131,17 +133,22 @@ fun BoxWithConstraintsScope.SettingsScreen() { onDeleteCloudUserData = { viewModel.onEvent(SettingsEvent.DeleteCloudUserData) }, + onSwitchLanguage = { + viewModel.onEvent(SettingsEvent.SwitchLanguage) + } ) } @ExperimentalFoundationApi @Composable +@Suppress("LongMethod") private fun BoxWithConstraintsScope.UI( currencyCode: String, theme: Theme, onSwitchTheme: () -> Unit, lockApp: Boolean, nameLocalAccount: String?, + languageOptionVisible: Boolean, onSetCurrency: (String) -> Unit, startDateOfMonth: Int = 1, showNotifications: Boolean = true, @@ -160,8 +167,8 @@ private fun BoxWithConstraintsScope.UI( onSetStartDateOfMonth: (Int) -> Unit = {}, onDeleteAllUserData: () -> Unit = {}, onDeleteCloudUserData: () -> Unit = {}, - - ) { + onSwitchLanguage: () -> Unit = {} +) { var currencyModalVisible by remember { mutableStateOf(false) } var nameModalVisible by remember { mutableStateOf(false) } var chooseStartDateOfMonthVisible by remember { mutableStateOf(false) } @@ -296,6 +303,18 @@ private fun BoxWithConstraintsScope.UI( // // Spacer(Modifier.height(12.dp)) + if (languageOptionVisible) { + SettingsDefaultButton( + icon = R.drawable.ic_vue_location_global, + text = stringResource(R.string.language), + description = Locale.getDefault().displayName + ) { + onSwitchLanguage() + } + + Spacer(Modifier.height(12.dp)) + } + SettingsDefaultButton( icon = R.drawable.ic_currency, text = stringResource(R.string.exchange_rates), @@ -1124,6 +1143,7 @@ private fun Preview(theme: Theme = Theme.LIGHT) { lockApp = false, currencyCode = "BGN", onSetCurrency = {}, + languageOptionVisible = true ) } } diff --git a/screen/settings/src/main/java/com/ivy/settings/SettingsState.kt b/screen/settings/src/main/java/com/ivy/settings/SettingsState.kt index 39384b95eb..c5f81e417d 100644 --- a/screen/settings/src/main/java/com/ivy/settings/SettingsState.kt +++ b/screen/settings/src/main/java/com/ivy/settings/SettingsState.kt @@ -12,5 +12,6 @@ data class SettingsState( val hideIncome: Boolean, val treatTransfersAsIncomeExpense: Boolean, val startDateOfMonth: String, - val progressState: Boolean + val progressState: Boolean, + val languageOptionVisible: Boolean ) diff --git a/screen/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt b/screen/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt index 96b61a0e15..d9117d845e 100644 --- a/screen/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt +++ b/screen/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt @@ -1,6 +1,11 @@ package com.ivy.settings import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -15,8 +20,8 @@ import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteSettingsDao import com.ivy.data.model.primitive.AssetCode import com.ivy.domain.RootScreen -import com.ivy.domain.usecase.exchange.SyncExchangeRatesUseCase import com.ivy.domain.usecase.csv.ExportCsvUseCase +import com.ivy.domain.usecase.exchange.SyncExchangeRatesUseCase import com.ivy.frp.monad.Res import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.LogoutLogic @@ -31,6 +36,7 @@ import com.ivy.wallet.domain.action.global.UpdateStartDayOfMonthAct import com.ivy.wallet.domain.action.settings.SettingsAct import com.ivy.widget.balance.WalletBalanceWidgetReceiver import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -51,6 +57,7 @@ class SettingsViewModel @Inject constructor( private val updateSettingsAct: UpdateSettingsAct, private val settingsWriter: WriteSettingsDao, private val exportCsvUseCase: ExportCsvUseCase, + @ApplicationContext private val context: Context ) : ComposeViewModel() { private val currencyCode = mutableStateOf("") @@ -80,7 +87,8 @@ class SettingsViewModel @Inject constructor( treatTransfersAsIncomeExpense = getTreatTransfersAsIncomeExpense(), startDateOfMonth = getStartDateOfMonth(), progressState = getProgressState(), - hideIncome = getHideIncome() + hideIncome = getHideIncome(), + languageOptionVisible = isLanguageOptionVisible() ) } @@ -195,6 +203,10 @@ class SettingsViewModel @Inject constructor( return progressState.value } + private fun isLanguageOptionVisible(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + } + override fun onEvent(event: SettingsEvent) { when (event) { is SettingsEvent.SetCurrency -> setCurrency(event.newCurrency) @@ -220,6 +232,7 @@ class SettingsViewModel @Inject constructor( SettingsEvent.DeleteCloudUserData -> deleteCloudUserData() SettingsEvent.DeleteAllUserData -> deleteAllUserData() + SettingsEvent.SwitchLanguage -> switchLanguage() } } @@ -383,4 +396,13 @@ class SettingsViewModel @Inject constructor( logoutLogic.logout() } } + + private fun switchLanguage() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.data = Uri.fromParts("package", context.packageName, null) + context.applicationContext.startActivity(intent) + } + } } diff --git a/shared/ui/core/src/main/res/values-ar/strings.xml b/shared/ui/core/src/main/res/values-ar/strings.xml index 45852f79cf..0d3095ad36 100644 --- a/shared/ui/core/src/main/res/values-ar/strings.xml +++ b/shared/ui/core/src/main/res/values-ar/strings.xml @@ -440,4 +440,5 @@ المجموع المحفظة المجموع المحفظة(مستبعد) وسوم البحث + لغة diff --git a/shared/ui/core/src/main/res/values-bg/strings.xml b/shared/ui/core/src/main/res/values-bg/strings.xml index 02b3406f3c..b91126d4c3 100644 --- a/shared/ui/core/src/main/res/values-bg/strings.xml +++ b/shared/ui/core/src/main/res/values-bg/strings.xml @@ -440,4 +440,5 @@ Общо БАЛАНС Общо БАЛАНС (изключен) Търсени етикети + Eзик \ No newline at end of file diff --git a/shared/ui/core/src/main/res/values-de/strings.xml b/shared/ui/core/src/main/res/values-de/strings.xml index 5708582db4..cd4f9472a9 100644 --- a/shared/ui/core/src/main/res/values-de/strings.xml +++ b/shared/ui/core/src/main/res/values-de/strings.xml @@ -487,4 +487,5 @@ Fortsetzen " !!!⚠️ACHTUNG: Ein Import kann zu doppelten Einträgen führen!!! Ivy Wallet kann diese NICHT automatisch entfernt und alle Einträge müssen manuell geprüft werden! Wenn du noch keine Einträge erfasst hast, kannst du diese Meldung ignorieren." Tags auswählen + Sprache \ No newline at end of file diff --git a/shared/ui/core/src/main/res/values-es/strings.xml b/shared/ui/core/src/main/res/values-es/strings.xml index 54cb088704..6a75e6f8a4 100644 --- a/shared/ui/core/src/main/res/values-es/strings.xml +++ b/shared/ui/core/src/main/res/values-es/strings.xml @@ -440,4 +440,5 @@ Total Balance Total Balance (excluido) Etiquetas de búsqueda + Idioma diff --git a/shared/ui/core/src/main/res/values-hi/strings.xml b/shared/ui/core/src/main/res/values-hi/strings.xml index 423f1fe41c..7d530995d8 100644 --- a/shared/ui/core/src/main/res/values-hi/strings.xml +++ b/shared/ui/core/src/main/res/values-hi/strings.xml @@ -440,4 +440,5 @@ कुल बैलेंस कुल बैलेंस (छोड़ा गया) \"खोज टैग्स + भाषा \ No newline at end of file diff --git a/shared/ui/core/src/main/res/values-id/strings.xml b/shared/ui/core/src/main/res/values-id/strings.xml index 0a54ba295e..9cefa48f12 100644 --- a/shared/ui/core/src/main/res/values-id/strings.xml +++ b/shared/ui/core/src/main/res/values-id/strings.xml @@ -443,4 +443,5 @@ Total dompet Total dompet (tidak termasuk) Tag Pencarian + Bahasa diff --git a/shared/ui/core/src/main/res/values-in/strings.xml b/shared/ui/core/src/main/res/values-in/strings.xml index 0a54ba295e..9cefa48f12 100644 --- a/shared/ui/core/src/main/res/values-in/strings.xml +++ b/shared/ui/core/src/main/res/values-in/strings.xml @@ -443,4 +443,5 @@ Total dompet Total dompet (tidak termasuk) Tag Pencarian + Bahasa diff --git a/shared/ui/core/src/main/res/values-it/strings.xml b/shared/ui/core/src/main/res/values-it/strings.xml index 632b55c7a1..02abb808c0 100644 --- a/shared/ui/core/src/main/res/values-it/strings.xml +++ b/shared/ui/core/src/main/res/values-it/strings.xml @@ -480,4 +480,5 @@ I tuoi dati saranno salvati localmente sul tuo dispositivo. Rischi di perdere i tuoi dati se disinstalli l\'app o se cambi dispositivo. Per prevenire perdite di dati, raccomandiamo di esportare regolarmente un backup dalle impostazioni. Sei in debito di: %1$s Ti sono dovuti %1$s + Lingua diff --git a/shared/ui/core/src/main/res/values-kn/strings.xml b/shared/ui/core/src/main/res/values-kn/strings.xml index f10724c7b9..34b8621c37 100644 --- a/shared/ui/core/src/main/res/values-kn/strings.xml +++ b/shared/ui/core/src/main/res/values-kn/strings.xml @@ -440,4 +440,5 @@ ಒಟ್ಟು ಮೊತ್ತ ಒಟ್ಟು ಮೊತ್ತ (ಹೊರತುಪಡಿಸಿ) ಹುಡುಕಿನ ಟ್ಯಾಗ್‌ಗಳು + ಭಾಷೆ diff --git a/shared/ui/core/src/main/res/values-mn/strings.xml b/shared/ui/core/src/main/res/values-mn/strings.xml index e6459c0479..0878010b49 100644 --- a/shared/ui/core/src/main/res/values-mn/strings.xml +++ b/shared/ui/core/src/main/res/values-mn/strings.xml @@ -440,4 +440,5 @@ Нийт ҮЛДЭГДЭЛ Нийт ҮЛДЭГДЭЛ (үлдэгдэл тооцохгүй) Хайлтын тагууд + Хэл diff --git a/shared/ui/core/src/main/res/values-pl/strings.xml b/shared/ui/core/src/main/res/values-pl/strings.xml index fe46d519dd..0ebcea5554 100644 --- a/shared/ui/core/src/main/res/values-pl/strings.xml +++ b/shared/ui/core/src/main/res/values-pl/strings.xml @@ -443,4 +443,5 @@ Łącznie Saldo Łącznie Saldo (wykluczony) Tagi wyszukiwania + Język diff --git a/shared/ui/core/src/main/res/values-pt-rBR/strings.xml b/shared/ui/core/src/main/res/values-pt-rBR/strings.xml index 49600ef628..16e488ad6f 100644 --- a/shared/ui/core/src/main/res/values-pt-rBR/strings.xml +++ b/shared/ui/core/src/main/res/values-pt-rBR/strings.xml @@ -487,4 +487,5 @@ Continuar "\n!!!⚠️AVISO: A importação pode duplicar transações!!!\nTransações duplicadas NÃO podem ser excluídas facilmente e você precisará remover manualmente cada uma delas! \nMotivo: podemos Não analisamos IDs de transação porque o Ivy Wallet funciona apenas com UUID e outros aplicativos não.\nSe você está começando do zero, não se preocupe. Por favor, ignore esta mensagem." Selecionar tags + Linguagem diff --git a/shared/ui/core/src/main/res/values-ru/strings.xml b/shared/ui/core/src/main/res/values-ru/strings.xml index 5093c3e3f6..eb3b2385f7 100644 --- a/shared/ui/core/src/main/res/values-ru/strings.xml +++ b/shared/ui/core/src/main/res/values-ru/strings.xml @@ -440,4 +440,5 @@ Итого БАЛАНС Итого БАЛАНС (исключенный) Теги поиска + Язык diff --git a/shared/ui/core/src/main/res/values-vi/strings.xml b/shared/ui/core/src/main/res/values-vi/strings.xml index bfb9f8686e..d4472ff768 100644 --- a/shared/ui/core/src/main/res/values-vi/strings.xml +++ b/shared/ui/core/src/main/res/values-vi/strings.xml @@ -486,4 +486,5 @@ Tiếp tục "\n!!!⚠️CẢNH BÁO: Việc tải tệp lên có thể gây ra trùng lặp các giao dịch!!!\nGiao dịch trùng lặp KHÔNG thể dễ dàng xóa và bạn sẽ cần phải xóa từng giao dịch một cách thủ công!\nLý do: Chúng tôi không thể phân tích các ID giao dịch vì Ivy Wallet chỉ hoạt động với UUID và các ứng dụng khác không.\nNếu bạn chọn Bắt đầu, không cần lo lắng - vui lòng bỏ qua thông báo này.\n " Chọn thẻ + Ngôn ngữ \ No newline at end of file diff --git a/shared/ui/core/src/main/res/values-zh-rCN/strings.xml b/shared/ui/core/src/main/res/values-zh-rCN/strings.xml index e4e986697c..b484c9ebc7 100644 --- a/shared/ui/core/src/main/res/values-zh-rCN/strings.xml +++ b/shared/ui/core/src/main/res/values-zh-rCN/strings.xml @@ -487,4 +487,5 @@ 继续 "\n!!!⚠️警告:导入可能会导致交易重复!!!\n重复的交易无法轻松删除,您需要手动删除每一笔重复的交易!\n原因:我们无法解析交易 ID,因为 Ivy Wallet 仅使用 UUID,而其他应用程序不使用。\n如果您是从头开始的,不用担心-请忽略此消息。" 选择标签 + 语言 diff --git a/shared/ui/core/src/main/res/values-zh-rTW/strings.xml b/shared/ui/core/src/main/res/values-zh-rTW/strings.xml index 1b677874ed..753d542a54 100644 --- a/shared/ui/core/src/main/res/values-zh-rTW/strings.xml +++ b/shared/ui/core/src/main/res/values-zh-rTW/strings.xml @@ -489,4 +489,5 @@ "\n!!!⚠️警告: 導入可能會重複交易!!!\n重複的交易無法輕易刪除,您需要手動刪除其中的每一項!\n原因: 我們無法解析交易 ID,因為 Ivy 錢包僅適用於 UUID,而其他應用程式則不能。\n如果您剛開始,不用擔心 -請忽略此訊息。" 選擇標籤 您的 %1$s %2$s 帳單已支付 + 语言 diff --git a/shared/ui/core/src/main/res/values/strings.xml b/shared/ui/core/src/main/res/values/strings.xml index efca692736..3e38674581 100644 --- a/shared/ui/core/src/main/res/values/strings.xml +++ b/shared/ui/core/src/main/res/values/strings.xml @@ -489,4 +489,5 @@ "\n!!!⚠️WARNING: Importing may duplicate transactions!!!\nDuplicate transactions can NOT be easily deleted and you'll need to remove manually each one of them! \nReason: We can't parse transaction ids because Ivy Wallet works only with UUID and other apps don't.\nIf you're starting fresh, no worries - kindly ignore this message." Select Tags Your bill for %1$s %2$s has been paid + Language From a1a58280595f4ddae33cd36078fe922f248d87b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 12:46:16 +0300 Subject: [PATCH 15/16] Automatic release: v2024.07.14 (190) (#3340) Co-authored-by: GitHub Actions --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a53ecdcf22..c9f59f4b90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +17,8 @@ paparazzi = "1.3.3" # Android min-sdk = "28" compile-sdk = "34" -version-name = "2024.07.11" -version-code = "189" +version-name = "2024.07.14" +version-code = "190" jvm-target = "17" From 1a46521c5d54b9e50d146cfbece339ce7b0ffb8a Mon Sep 17 00:00:00 2001 From: Lex Leontiev Date: Wed, 17 Jul 2024 08:26:40 +0100 Subject: [PATCH 16/16] Fix issue 3342 (#3346) * add duplicate button to the edit transaction screen * duplicate transaction on click the duplicate button and close the transaction screen --------- Co-authored-by: lexleontiev --- gradle/libs.versions.toml | 2 + .../ivy/transaction/EditTransactionEvent.kt | 1 + .../ivy/transaction/EditTransactionScreen.kt | 9 ++++- .../transaction/EditTransactionViewModel.kt | 19 +++++++++ .../com/ivy/planned/edit/EditPlannedScreen.kt | 4 +- .../legacy/ui/component/edit/core/Toolbar.kt | 40 +++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9f59f4b90..576117b0ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,6 +76,7 @@ compose-animation = { module = "androidx.compose.animation:animation", version.r compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "compose-material3" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } compose-runtime-livedate-temp = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose" } compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } @@ -190,6 +191,7 @@ compose = [ "compose-foundation", "compose-material3", "compose-material3-windowsize", + "compose-material-icons-extended", "compose-runtime", "compose-ui", "compose-activity", diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt index 2f3baad975..763eb689e1 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt @@ -24,6 +24,7 @@ sealed interface EditTransactionEvent { data class OnSetTransactionType(val newTransactionType: TransactionType) : EditTransactionEvent data object OnPayPlannedPayment : EditTransactionEvent data object Delete : EditTransactionEvent + data object Duplicate : EditTransactionEvent data class CreateCategory(val data: CreateCategoryData) : EditTransactionEvent data class EditCategory(val updatedCategory: Category) : EditTransactionEvent data class CreateAccount(val data: CreateAccountData) : EditTransactionEvent diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt index 0370fbd973..3dd90b588c 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt @@ -172,6 +172,9 @@ fun BoxWithConstraintsScope.EditTransactionScreen(screen: EditTransactionScreen) onDelete = { viewModel.onEvent(EditTransactionEvent.Delete) }, + onDuplicate = { + viewModel.onEvent(EditTransactionEvent.Duplicate) + }, onCreateAccount = { viewModel.onEvent(EditTransactionEvent.CreateAccount(it)) }, @@ -223,6 +226,7 @@ private fun BoxWithConstraintsScope.UI( onSave: (closeScreen: Boolean) -> Unit, onSetHasChanges: (hasChanges: Boolean) -> Unit, onDelete: () -> Unit, + onDuplicate: () -> Unit, onCreateAccount: (CreateAccountData) -> Unit, onExchangeRateChange: (Double?) -> Unit = { }, onTagOperation: (EditTransactionEvent.TagEvent) -> Unit = {}, @@ -290,7 +294,9 @@ private fun BoxWithConstraintsScope.UI( }, onChangeTransactionTypeModal = { changeTransactionTypeModalVisible = true - } + }, + showDuplicateButton = true, + onDuplicate = onDuplicate ) Spacer(Modifier.height(32.dp)) @@ -696,6 +702,7 @@ private fun BoxWithConstraintsScope.Preview(isDark: Boolean = false) { onSave = {}, onSetHasChanges = {}, onDelete = {}, + onDuplicate = {}, onCreateAccount = { }, onSetDate = {}, onSetTime = {}, diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt index 10ce2f4bb4..1b70a8af4e 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt @@ -313,6 +313,7 @@ class EditTransactionViewModel @Inject constructor( is EditTransactionEvent.CreateAccount -> createAccount(event.data) is EditTransactionEvent.CreateCategory -> createCategory(event.data) EditTransactionEvent.Delete -> delete() + EditTransactionEvent.Duplicate -> duplicate() is EditTransactionEvent.EditCategory -> editCategory(event.updatedCategory) is EditTransactionEvent.OnAccountChanged -> onAccountChanged(event.newAccount) is EditTransactionEvent.OnAmountChanged -> onAmountChanged(event.newAmount) @@ -605,6 +606,24 @@ class EditTransactionViewModel @Inject constructor( } } + private fun duplicate() { + viewModelScope.launch { + ioThread { + loadedTransaction() + .copy( + id = UUID.randomUUID(), + dateTime = timeNowLocal() + ) + .toDomain(transactionMapper) + ?.let { + transactionRepo.save(it) + } + refreshWidget(WalletBalanceWidgetReceiver::class.java) + } + closeScreen() + } + } + private fun createCategory(data: CreateCategoryData) { viewModelScope.launch { categoryCreator.createCategory(data) { diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt index 91dfe9b9b1..39efe51427 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedScreen.kt @@ -107,7 +107,9 @@ private fun BoxWithConstraintsScope.UI( }, onChangeTransactionTypeModal = { onEvent(EditPlannedScreenEvent.OnTransactionTypeModalVisible(true)) - } + }, + showDuplicateButton = false, + onDuplicate = {} ) Spacer(Modifier.height(32.dp)) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/Toolbar.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/Toolbar.kt index af2519ca39..87667f1dba 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/Toolbar.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/Toolbar.kt @@ -1,14 +1,27 @@ package com.ivy.wallet.ui.edit.core +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.CopyAll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.ivy.base.model.TransactionType +import com.ivy.design.l0_system.UI +import com.ivy.design.l0_system.asBrush import com.ivy.navigation.navigation import com.ivy.ui.R import com.ivy.wallet.ui.theme.components.CloseButton @@ -24,6 +37,9 @@ fun Toolbar( onDeleteTrnModal: () -> Unit, onChangeTransactionTypeModal: () -> Unit, + + showDuplicateButton: Boolean, + onDuplicate: () -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically @@ -66,6 +82,30 @@ fun Toolbar( } if (initialTransactionId != null) { + if (showDuplicateButton) { + OutlinedIconButton( + modifier = Modifier + .size(48.dp) + .background(Color.Transparent, CircleShape) + .testTag("duplicate_button"), + shape = CircleShape, + colors = IconButtonDefaults.outlinedIconButtonColors() + .copy(contentColor = UI.colors.medium), + border = IconButtonDefaults.outlinedIconButtonBorder(enabled = true) + .copy(width = 2.dp, brush = UI.colors.medium.asBrush()), + onClick = onDuplicate + ) { + Icon( + modifier = Modifier.padding(6.dp), + imageVector = Icons.Sharp.CopyAll, + contentDescription = "duplicate_button", + tint = UI.colors.pureInverse + ) + } + + Spacer(Modifier.width(12.dp)) + } + DeleteButton( hasShadow = false ) {