From 3430fb494c35b1523933699e214017bac2a520d9 Mon Sep 17 00:00:00 2001 From: Richard <54624799+Rick-AB@users.noreply.github.com> Date: Tue, 13 Feb 2024 12:29:38 +0100 Subject: [PATCH 01/10] Fix issue 2932 (#2957) * updated fake category dao * fake category dao test implementation --- .../ivy/data/db/dao/fake/FakeCategoryDao.kt | 17 +- .../com/ivy/data/dao/FakeCategoryDaoTest.kt | 423 ++++++++++++++++++ 2 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 shared/data/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt diff --git a/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt b/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt index d95e86ded1..0d24cb0ba3 100644 --- a/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt +++ b/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeCategoryDao.kt @@ -11,7 +11,7 @@ class FakeCategoryDao : CategoryDao, WriteCategoryDao { private val items = mutableListOf() override suspend fun findAll(deleted: Boolean): List { - return items + return items.filter { it.isDeleted == deleted } } override suspend fun findById(id: UUID): CategoryEntity? { @@ -23,7 +23,12 @@ class FakeCategoryDao : CategoryDao, WriteCategoryDao { } override suspend fun save(value: CategoryEntity) { - items.add(value) + val existingItemIndex = items.indexOfFirst { it.id == value.id } + if (existingItemIndex > -1) { + items[existingItemIndex] = value + } else { + items.add(value) + } } override suspend fun saveMany(values: List) { @@ -31,7 +36,13 @@ class FakeCategoryDao : CategoryDao, WriteCategoryDao { } override suspend fun flagDeleted(id: UUID) { - TODO("Not yet implemented") + items.replaceAll { categoryEntity -> + if (categoryEntity.id == id) { + categoryEntity.copy(isDeleted = true) + } else { + categoryEntity + } + } } override suspend fun deleteById(id: UUID) { diff --git a/shared/data/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt b/shared/data/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt new file mode 100644 index 0000000000..2877cb3941 --- /dev/null +++ b/shared/data/src/test/java/com/ivy/data/dao/FakeCategoryDaoTest.kt @@ -0,0 +1,423 @@ +package com.ivy.data.dao + +import com.ivy.data.db.dao.fake.FakeCategoryDao +import com.ivy.data.db.entity.CategoryEntity +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import java.util.UUID + +class FakeCategoryDaoTest : FreeSpec({ + fun newCategoryDao() = FakeCategoryDao() + + "find all" - { + "not deleted" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findAll(false) + + // then + res shouldBe listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ) + ) + } + + "deleted" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findAll(true) + + // then + res shouldBe listOf( + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + ) + } + + "empty list" { + // given + val categoryDao = newCategoryDao() + val categories = emptyList() + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findAll(false) + + // then + res shouldBe emptyList() + } + } + + "find by id" - { + "existing id" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val category2 = CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + category2 + ) + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findById(id2) + + // then + res shouldBe category2 + } + + "not existing id" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findById(UUID.randomUUID()) + + // then + res shouldBe null + } + } + + "find max order num" - { + "of categories" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findMaxOrderNum() + + // then + res shouldBe 2.0 + } + + "of empty list" - { + // given + val categoryDao = newCategoryDao() + val categories = emptyList() + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findMaxOrderNum() + + // then + res shouldBe null + } + } + + "save" - { + "create new" { + // given + val categoryDao = newCategoryDao() + val id = UUID.randomUUID() + val category = CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id + ) + + // when + categoryDao.save(category) + val res = categoryDao.findById(id) + + // then + res shouldBe category + } + + "update existing" { + // given + val categoryDao = newCategoryDao() + val id = UUID.randomUUID() + val category = CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id + ) + val updated = category.copy(name = "My Home") + + // when + categoryDao.save(category) + categoryDao.save(updated) + val res = categoryDao.findAll(deleted = false) + + // then + res shouldBe listOf(updated) + } + } + + "save many" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = false, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + val res = categoryDao.findAll(false) + + // then + res shouldBe categories + } + + "flag deleted" - { + "existing id" { + // given + val categoryDao = newCategoryDao() + val id = UUID.randomUUID() + val category = CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id + ) + + // when + categoryDao.save(category) + categoryDao.flagDeleted(id) + val res = categoryDao.findById(id) + + // then + res shouldBe category.copy(isDeleted = true) + } + + "not existing id" { + // given + val categoryDao = newCategoryDao() + val id = UUID.randomUUID() + val category = CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id + ) + + // when + categoryDao.save(category) + categoryDao.flagDeleted(UUID.randomUUID()) + val res = categoryDao.findById(id) + + // then + res shouldBe category + } + } + + "delete by id" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = false, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + categoryDao.deleteById(id2) + val res = categoryDao.findAll(false) + + // then + res shouldBe listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ) + ) + } + + "delete all" { + // given + val categoryDao = newCategoryDao() + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val categories = listOf( + CategoryEntity( + name = "Home", + color = 42, + icon = null, + orderNum = 1.0, + isDeleted = false, + id = id1 + ), + CategoryEntity( + name = "Fun", + color = 42, + icon = null, + orderNum = 2.0, + isDeleted = true, + id = id2 + ) + ) + + // when + categoryDao.saveMany(categories) + categoryDao.deleteAll() + val notDeleted = categoryDao.findAll(false) + val deleted = categoryDao.findAll(true) + + // then + notDeleted shouldBe emptyList() + deleted shouldBe emptyList() + } +}) From 26bb0dc414d6f904c4313d9f97aeb5334e8fb5b6 Mon Sep 17 00:00:00 2001 From: Richard <54624799+Rick-AB@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:18:50 +0100 Subject: [PATCH 02/10] Fix issue 2933 (#2959) * updated fake transaction dao * implement fake transaction dao test --- .../data/db/dao/fake/FakeTransactionDao.kt | 14 +- .../ivy/data/dao/FakeTransactionDaoTest.kt | 3078 +++++++++++++++++ 2 files changed, 3088 insertions(+), 4 deletions(-) create mode 100644 shared/data/src/test/java/com/ivy/data/dao/FakeTransactionDaoTest.kt diff --git a/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt b/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt index 12c9212fa9..1ac7b71d7b 100644 --- a/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt +++ b/shared/data/src/main/java/com/ivy/data/db/dao/fake/FakeTransactionDao.kt @@ -68,6 +68,7 @@ class FakeTransactionDao : TransactionDao, WriteTransactionDao { return items.filter { val dateTime = it.dateTime ?: return@filter false it.type == TransactionType.TRANSFER && + it.toAccountId == toAccountId && isBetween(dateTime, startDate, endDate) && !it.isDeleted }.sortedByDescending { it.dateTime } @@ -292,7 +293,12 @@ class FakeTransactionDao : TransactionDao, WriteTransactionDao { } override suspend fun save(value: TransactionEntity) { - items.add(value) + val existingItemIndex = items.indexOfFirst { it.id == value.id } + if (existingItemIndex > -1) { + items[existingItemIndex] = value + } else { + items.add(value) + } } override suspend fun saveMany(values: List) { @@ -300,7 +306,7 @@ class FakeTransactionDao : TransactionDao, WriteTransactionDao { } override suspend fun flagDeleted(id: UUID) { - items.map { + items.replaceAll { if (it.id == id) { it.copy(isDeleted = true) } else { @@ -310,7 +316,7 @@ class FakeTransactionDao : TransactionDao, WriteTransactionDao { } override suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) { - items.map { + items.replaceAll { if (it.recurringRuleId == recurringRuleId && it.dateTime == null) { it.copy(isDeleted = true) } else { @@ -320,7 +326,7 @@ class FakeTransactionDao : TransactionDao, WriteTransactionDao { } override suspend fun flagDeletedByAccountId(accountId: UUID) { - items.map { + items.replaceAll { if (it.accountId == accountId) { it.copy(isDeleted = true) } else { diff --git a/shared/data/src/test/java/com/ivy/data/dao/FakeTransactionDaoTest.kt b/shared/data/src/test/java/com/ivy/data/dao/FakeTransactionDaoTest.kt new file mode 100644 index 0000000000..c75b7dfaba --- /dev/null +++ b/shared/data/src/test/java/com/ivy/data/dao/FakeTransactionDaoTest.kt @@ -0,0 +1,3078 @@ +package com.ivy.data.dao + +import com.ivy.base.model.TransactionType +import com.ivy.data.db.dao.fake.FakeTransactionDao +import com.ivy.data.db.entity.TransactionEntity +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDateTime +import java.util.UUID + +@Suppress("LargeClass") +class FakeTransactionDaoTest : FreeSpec({ + fun newTransactionDao() = FakeTransactionDao() + + "find all" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + + // when + val res = transactionDao.findAll() + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + val res = transactionDao.findAll() + + // then + res shouldBe listOf(transfer1, expense1, income1) + } + } + + "find all limit 1" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + + // when + val res = transactionDao.findAll_LIMIT_1() + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + val res = transactionDao.findAll_LIMIT_1() + + // then + res shouldBe listOf(transfer1) + } + } + + "find all transfer to account" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val toAccountId = UUID.randomUUID() + + // when + val res = + transactionDao.findAllTransfersToAccount(toAccountId) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = UUID.randomUUID(), + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1, transfer2)) + val res = transactionDao.findAllTransfersToAccount(toAccountId) + + // then + res shouldBe listOf(transfer1) + } + } + + "find all transfer to account and between" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val toAccountId = UUID.randomUUID() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + + // when + val res = + transactionDao.findAllTransfersToAccountBetween(toAccountId, startDate, endDate) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = UUID.randomUUID(), + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1, transfer2)) + + val res = + transactionDao.findAllTransfersToAccountBetween(toAccountId, startDate, endDate) + + // then + res shouldBe listOf(transfer1) + } + } + + "find all between" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + + // when + val res = transactionDao.findAllBetween(startDate, endDate) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.plusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.findAllBetween(startDate, endDate) + + // then + res shouldBe listOf(expense1, income1, transfer1) + } + } + + "find all by account and between" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + + // when + val res = transactionDao.findAllByAccountAndBetween(accountId, startDate, endDate) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.plusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.findAllByAccountAndBetween(accountId, startDate, endDate) + + // then + res shouldBe listOf(expense1, income1, transfer1) + } + } + + "find all by category and between" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val categoryId = UUID.randomUUID() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + + // when + val res = transactionDao.findAllByCategoryAndBetween(categoryId, startDate, endDate) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val categoryId = UUID.randomUUID() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = endDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.plusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = UUID.randomUUID(), + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + val res = transactionDao.findAllByCategoryAndBetween(categoryId, startDate, endDate) + + // then + res shouldBe listOf(expense1, income1, transfer1) + } + } + + "find all unspecified and between" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + + // when + val res = transactionDao.findAllUnspecifiedAndBetween(startDate, endDate) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = UUID.randomUUID(), + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.plusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = UUID.randomUUID(), + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.findAllUnspecifiedAndBetween(startDate, endDate) + + // then + res shouldBe listOf(expense1, income1, transfer1) + } + } + + "find all to account and between" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val toAccountId = UUID.randomUUID() + + // when + val res = transactionDao.findAllToAccountAndBetween(toAccountId, startDate, endDate) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer3 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = UUID.randomUUID(), + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = UUID.randomUUID(), + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1, transfer2, transfer3)) + val res = transactionDao.findAllToAccountAndBetween(toAccountId, startDate, endDate) + + // then + res shouldBe listOf(transfer1) + } + } + + "find all by recurring rule id" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val recurringRuleId = UUID.randomUUID() + + // when + val res = transactionDao.findAllByRecurringRuleId(recurringRuleId) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val recurringRuleId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findAllByRecurringRuleId(recurringRuleId) + + // then + res shouldBe listOf(expense1, income1) + } + } + + "find all between and by recurring rule id" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val recurringRuleId = UUID.randomUUID() + + // when + val res = + transactionDao.findAllBetweenAndRecurringRuleId(startDate, endDate, recurringRuleId) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val startDate = LocalDateTime.now().minusDays(7) + val endDate = LocalDateTime.now() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val recurringRuleId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate, + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = endDate.plusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate, + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = startDate.minusDays(1), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = + transactionDao.findAllBetweenAndRecurringRuleId(startDate, endDate, recurringRuleId) + + // then + res shouldBe listOf(expense1, income1, transfer1) + } + } + + "find by id" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val transactionId = UUID.randomUUID() + + // when + val res = transactionDao.findById(transactionId) + + // then + res shouldBe null + } + + "existing id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val transactionId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = transactionId, + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findById(transactionId) + + // then + res shouldBe income1 + } + + "non existing id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findById(UUID.randomUUID()) + + // then + res shouldBe null + } + } + + "count happened transactions" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + + // when + val res = transactionDao.countHappenedTransactions() + + // then + res shouldBe 0 + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.countHappenedTransactions() + + // then + res shouldBe 3 + } + } + + "find all by title matching pattern" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val pattern = "Transaction" + + // when + val res = transactionDao.findAllByTitleMatchingPattern(pattern) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val pattern = "Transaction" + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "A new thing", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Bought something", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.findAllByTitleMatchingPattern(pattern) + + // then + res shouldBe listOf(transfer1, expense1, income1) + } + } + + "count by title matching pattern" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val pattern = "Transaction" + + // when + val res = transactionDao.countByTitleMatchingPattern(pattern) + + // then + res shouldBe 0 + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val pattern = "Transaction" + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "A new thing", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Bought something", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.countByTitleMatchingPattern(pattern) + + // then + res shouldBe 3 + } + } + + "count by title matching pattern and category id" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val pattern = "Transaction" + val categoryId = UUID.randomUUID() + + // when + val res = transactionDao.countByTitleMatchingPatternAndCategoryId(pattern, categoryId) + + // then + res shouldBe 0 + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val pattern = "Transaction" + val categoryId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "A new thing", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Bought something", + description = "Desc", + categoryId = UUID.randomUUID(), + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.countByTitleMatchingPatternAndCategoryId(pattern, categoryId) + + // then + res shouldBe 3 + } + } + + "count by title matching pattern and account id" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val pattern = "Transaction" + val accountId = UUID.randomUUID() + + // when + val res = transactionDao.countByTitleMatchingPatternAndAccountId(pattern, accountId) + + // then + res shouldBe 0 + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val pattern = "Transaction" + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "A new thing", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = UUID.randomUUID(), + title = "Transaction 1", + description = "Desc", + categoryId = UUID.randomUUID(), + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + val res = transactionDao.countByTitleMatchingPatternAndAccountId(pattern, accountId) + + // then + res shouldBe 4 + } + } + + "find all by category" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val categoryId = UUID.randomUUID() + + // when + val res = transactionDao.findAllByCategory(categoryId) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val categoryId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.findAllByCategory(categoryId) + + // then + res shouldBe listOf(transfer1, expense1, income1) + } + } + + "find all by account" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + + // when + val res = transactionDao.findAllByAccount(accountId) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val categoryId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val income2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val expense2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = categoryId, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + val transfer2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = true, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany( + listOf( + income1, + income2, + expense1, + expense2, + transfer1, + transfer2 + ) + ) + + val res = transactionDao.findAllByAccount(accountId) + + // then + res shouldBe listOf(transfer1, expense1, income1) + } + } + + "find loan transaction" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val loanId = UUID.randomUUID() + + // when + val res = transactionDao.findLoanTransaction(loanId) + + // then + res shouldBe null + } + + "existing id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val loanId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = loanId, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = UUID.randomUUID(), + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findLoanTransaction(loanId) + + // then + res shouldBe income1 + } + + "non existing id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val loanId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = loanId, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = UUID.randomUUID(), + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findLoanTransaction(UUID.randomUUID()) + + // then + res shouldBe null + } + } + + "find loan record transaction" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val loanRecordId = UUID.randomUUID() + + // when + val res = transactionDao.findLoanRecordTransaction(loanRecordId) + + // then + res shouldBe null + } + + "existing id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val loanRecordId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = loanRecordId, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = UUID.randomUUID(), + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findLoanRecordTransaction(loanRecordId) + + // then + res shouldBe income1 + } + + "non existing id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val loanRecordId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = loanRecordId, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = UUID.randomUUID(), + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findLoanRecordTransaction(UUID.randomUUID()) + + // then + res shouldBe null + } + } + + "find all by loan id" - { + "empty transactions" { + // given + val transactionDao = newTransactionDao() + val loanId = UUID.randomUUID() + + // when + val res = transactionDao.findAllByLoanId(loanId) + + // then + res shouldBe emptyList() + } + + "non empty transactions" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val toAccountId = UUID.randomUUID() + val loanId = UUID.randomUUID() + + val income1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = loanId, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val expense1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = loanId, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + val transfer1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + toAccountId = toAccountId, + toAmount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.saveMany(listOf(income1, expense1, transfer1)) + val res = transactionDao.findAllByLoanId(loanId) + + // then + res shouldBe listOf(expense1, income1) + } + } + + "save" - { + "create new" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transaction = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.TRANSFER + ) + + // when + transactionDao.save(transaction) + val res = transactionDao.findById(transaction.id) + + // then + res shouldBe transaction + } + + "update existing" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transactionId = UUID.randomUUID() + + val transaction = TransactionEntity( + id = transactionId, + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + // when + transactionDao.save(transaction) + transactionDao.save(transaction.copy(title = "New Transaction")) + val res = transactionDao.findById(transaction.id) + + // then + res shouldBe transaction.copy(title = "New Transaction") + } + } + + "save many" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transactions = listOf( + TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ), + TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + ) + + // when + transactionDao.saveMany(transactions) + val res = transactionDao.findAll() + + // then + res shouldBe transactions.sortedByDescending { it.dateTime } + } + + "flag deleted" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transaction = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + // when + transactionDao.save(transaction) + transactionDao.flagDeleted(transaction.id) + val res = transactionDao.findById(transaction.id) + + // then + res shouldBe transaction.copy(isDeleted = true) + } + + "flag deleted by recurring rule id and no date" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val recurringRuleId = UUID.randomUUID() + val transaction = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = null, + loanId = null, + loanRecordId = null, + recurringRuleId = recurringRuleId, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + // when + transactionDao.save(transaction) + transactionDao.flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId) + val res = transactionDao.findById(transaction.id) + + // then + res shouldBe transaction.copy(isDeleted = true) + } + + "flag deleted by account" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transactions = listOf( + TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ), + TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + ) + + // when + transactionDao.saveMany(transactions) + transactionDao.flagDeletedByAccountId(accountId) + val res = transactionDao.findByIsSyncedAndIsDeleted(synced = false, deleted = true) + + // then + res shouldBe transactions.map { it.copy(isDeleted = true) } + .sortedByDescending { it.dateTime } + } + + "delete by id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transaction = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + // when + transactionDao.save(transaction) + transactionDao.deleteById(transaction.id) + val res = transactionDao.findById(transaction.id) + + // then + res shouldBe null + } + + "delete by account id" { + // given + val transactionDao = newTransactionDao() + val accountId = UUID.randomUUID() + val transactions = listOf( + TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ), + TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + ) + + // when + transactionDao.saveMany(transactions) + transactionDao.deleteAllByAccountId(accountId) + val res = transactionDao.findAllByAccount(accountId) + + // then + res shouldBe emptyList() + } + + "delete all" { + // given + val transactionDao = newTransactionDao() + val accountId1 = UUID.randomUUID() + val accountId2 = UUID.randomUUID() + val transaction1 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId1, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.INCOME + ) + + val transaction2 = TransactionEntity( + id = UUID.randomUUID(), + accountId = accountId2, + title = "Transaction 1", + description = "Desc", + categoryId = null, + dateTime = LocalDateTime.now(), + loanId = null, + loanRecordId = null, + recurringRuleId = null, + isDeleted = false, + amount = 100.0, + type = TransactionType.EXPENSE + ) + + // when + transactionDao.saveMany(listOf(transaction1, transaction2)) + transactionDao.deleteAll() + val res = transactionDao.findAll() + + // then + res shouldBe emptyList() + } +}) From 8f86f6e922e061144cbfdfde317fbe23b4d0e3f3 Mon Sep 17 00:00:00 2001 From: Richard <54624799+Rick-AB@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:08:09 +0100 Subject: [PATCH 03/10] moved search query to viewmodel and added it to SearchState (#2960) --- screen/search/src/main/java/com/ivy/search/SearchScreen.kt | 3 ++- screen/search/src/main/java/com/ivy/search/SearchState.kt | 1 + .../search/src/main/java/com/ivy/search/SearchViewModel.kt | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/screen/search/src/main/java/com/ivy/search/SearchScreen.kt b/screen/search/src/main/java/com/ivy/search/SearchScreen.kt index 50337b9c5d..6fcf272e0a 100644 --- a/screen/search/src/main/java/com/ivy/search/SearchScreen.kt +++ b/screen/search/src/main/java/com/ivy/search/SearchScreen.kt @@ -60,7 +60,7 @@ private fun SearchUi( val listState = rememberLazyListState() var searchQueryTextFieldValue by remember { - mutableStateOf(selectEndTextFieldValue("")) + mutableStateOf(selectEndTextFieldValue(uiState.searchQuery)) } SearchInput( @@ -126,6 +126,7 @@ private fun Preview() { IvyPreview { SearchUi( uiState = SearchState( + searchQuery = "Transaction", transactions = persistentListOf(), baseCurrency = "", accounts = persistentListOf(), diff --git a/screen/search/src/main/java/com/ivy/search/SearchState.kt b/screen/search/src/main/java/com/ivy/search/SearchState.kt index 83f224e629..3738f9f81e 100644 --- a/screen/search/src/main/java/com/ivy/search/SearchState.kt +++ b/screen/search/src/main/java/com/ivy/search/SearchState.kt @@ -6,6 +6,7 @@ import com.ivy.legacy.datamodel.Category import kotlinx.collections.immutable.ImmutableList data class SearchState( + val searchQuery: String, val transactions: ImmutableList, val baseCurrency: String, val accounts: ImmutableList, diff --git a/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt b/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt index a26172734f..524687502e 100644 --- a/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt +++ b/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt @@ -36,14 +36,16 @@ class SearchViewModel @Inject constructor( private val baseCurrency = mutableStateOf(getDefaultFIATCurrency().currencyCode) private val accounts = mutableStateOf>(persistentListOf()) private val categories = mutableStateOf>(persistentListOf()) + private val searchQuery = mutableStateOf("") @Composable override fun uiState(): SearchState { LaunchedEffect(Unit) { - search("") + search(searchQuery.value) } return SearchState( + searchQuery = searchQuery.value, transactions = transactions.value, baseCurrency = baseCurrency.value, accounts = accounts.value, @@ -58,6 +60,7 @@ class SearchViewModel @Inject constructor( } private fun search(query: String) { + searchQuery.value = query val normalizedQuery = query.lowercase().trim() viewModelScope.launch { @@ -65,7 +68,7 @@ class SearchViewModel @Inject constructor( val filteredTransactions = allTrnsAct(Unit) .filter { transaction -> transaction.title.matchesQuery(normalizedQuery) || - transaction.description.matchesQuery(normalizedQuery) + transaction.description.matchesQuery(normalizedQuery) } trnsWithDateDivsAct( TrnsWithDateDivsAct.Input( From 50f044658069f385c951e66e02e35e8d6dbd5f96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 00:59:31 +0200 Subject: [PATCH 04/10] Bump the ivy-wallet group with 9 updates (#2952) * Bump the ivy-wallet group with 9 updates Bumps the ivy-wallet group with 9 updates: | Package | From | To | | --- | --- | --- | | androidx.compose.animation:animation | `1.6.0` | `1.6.1` | | androidx.compose.foundation:foundation | `1.6.0` | `1.6.1` | | androidx.compose.runtime:runtime | `1.6.0` | `1.6.1` | | androidx.compose.runtime:runtime-livedata | `1.6.0` | `1.6.1` | | androidx.compose.ui:ui | `1.6.0` | `1.6.1` | | androidx.compose.ui:ui-tooling | `1.6.0` | `1.6.1` | | androidx.compose.foundation:foundation | `1.6.0` | `1.6.1` | | androidx.compose.runtime:runtime | `1.6.0` | `1.6.1` | | androidx.compose.runtime:runtime-livedata | `1.6.0` | `1.6.1` | | androidx.compose.ui:ui | `1.6.0` | `1.6.1` | | androidx.compose.ui:ui-tooling | `1.6.0` | `1.6.1` | | com.google.gms:google-services | `4.4.0` | `4.4.1` | | [com.google.firebase:firebase-crashlytics](https://github.com/firebase/firebase-android-sdk) | `18.6.1` | `18.6.2` | | [com.github.Ivy-Apps:detekt-explicit](https://github.com/Ivy-Apps/detekt-explicit) | `v0.0.5` | `v0.0.6` | Updates `androidx.compose.animation:animation` from 1.6.0 to 1.6.1 Updates `androidx.compose.foundation:foundation` from 1.6.0 to 1.6.1 Updates `androidx.compose.runtime:runtime` from 1.6.0 to 1.6.1 Updates `androidx.compose.runtime:runtime-livedata` from 1.6.0 to 1.6.1 Updates `androidx.compose.ui:ui` from 1.6.0 to 1.6.1 Updates `androidx.compose.ui:ui-tooling` from 1.6.0 to 1.6.1 Updates `androidx.compose.foundation:foundation` from 1.6.0 to 1.6.1 Updates `androidx.compose.runtime:runtime` from 1.6.0 to 1.6.1 Updates `androidx.compose.runtime:runtime-livedata` from 1.6.0 to 1.6.1 Updates `androidx.compose.ui:ui` from 1.6.0 to 1.6.1 Updates `androidx.compose.ui:ui-tooling` from 1.6.0 to 1.6.1 Updates `com.google.gms:google-services` from 4.4.0 to 4.4.1 Updates `com.google.firebase:firebase-crashlytics` from 18.6.1 to 18.6.2 - [Changelog](https://github.com/firebase/firebase-android-sdk/blob/master/docs/make_release_notes.py) - [Commits](https://github.com/firebase/firebase-android-sdk/commits) Updates `com.github.Ivy-Apps:detekt-explicit` from v0.0.5 to v0.0.6 - [Commits](https://github.com/Ivy-Apps/detekt-explicit/compare/v0.0.5...v0.0.6) --- updated-dependencies: - dependency-name: androidx.compose.animation:animation dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.foundation:foundation dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.runtime:runtime dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.runtime:runtime-livedata dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.ui:ui dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.ui:ui-tooling dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.foundation:foundation dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.runtime:runtime dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.runtime:runtime-livedata dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.ui:ui dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: androidx.compose.ui:ui-tooling dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: com.google.gms:google-services dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: com.google.firebase:firebase-crashlytics dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: com.github.Ivy-Apps:detekt-explicit dependency-type: direct:production dependency-group: ivy-wallet ... Signed-off-by: dependabot[bot] * Update Ivy Detekt to 0.0.7 to fix the `UnnecessaryPassThroughClass` issue * Disable the code coverage task because it's slow and flaky --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Iliyan Germanov --- .github/workflows/test.yml | 62 +++++++++++++++++++------------------- config/detekt/config.yml | 2 ++ gradle/libs.versions.toml | 8 ++--- 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 759b6d83a3..1ea7bf62ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,34 +32,34 @@ jobs: - name: Run unit tests run: ./gradlew testDebugUnitTest - code_coverage: - needs: test - runs-on: ubuntu-latest - steps: - - name: Checkout GIT - uses: actions/checkout@v4 - - - name: Setup Java SDK - uses: actions/setup-java@v4 - with: - distribution: 'adopt' - java-version: '18' - - - name: Enable Gradle Wrapper caching (optimization) - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Code Coverage - run: ./gradlew koverHtmlReport - - - name: Upload Code Coverage report - uses: actions/upload-artifact@v4 - with: - name: code-coverage-report - path: app/build/artifacts/reports/kover/coverageResults/* +# code_coverage: +# needs: test +# runs-on: ubuntu-latest +# steps: +# - name: Checkout GIT +# uses: actions/checkout@v4 +# +# - name: Setup Java SDK +# uses: actions/setup-java@v4 +# with: +# distribution: 'adopt' +# java-version: '18' +# +# - name: Enable Gradle Wrapper caching (optimization) +# uses: actions/cache@v4 +# with: +# path: | +# ~/.gradle/caches +# ~/.gradle/wrapper +# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} +# restore-keys: | +# ${{ runner.os }}-gradle- +# +# - name: Code Coverage +# run: ./gradlew koverHtmlReport +# +# - name: Upload Code Coverage report +# uses: actions/upload-artifact@v4 +# with: +# name: code-coverage-report +# path: app/build/artifacts/reports/kover/coverageResults/* diff --git a/config/detekt/config.yml b/config/detekt/config.yml index 0dbba94210..db972225a0 100644 --- a/config/detekt/config.yml +++ b/config/detekt/config.yml @@ -1117,3 +1117,5 @@ IvyExplicit: active: true NoImplicitFunctionReturnType: active: true + UnnecessaryPassThroughClass: + active: true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cf22f1d88c..cff155de33 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ktor = "2.3.8" arrow = "1.2.1" kotest = "5.8.0" # https://developer.android.com/jetpack/androidx/releases/compose -compose = "1.6.0" +compose = "1.6.1" # https://developer.android.com/jetpack/androidx/releases/compose-kotlin compose-compiler = "1.5.8" # It's used! Use compatible with Kotlin one compose-material3 = "1.2.0" @@ -101,11 +101,11 @@ hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.re hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } # Google -google-services-plugin = { module = "com.google.gms:google-services", version = "4.4.0" } +google-services-plugin = { module = "com.google.gms:google-services", version = "4.4.1" } google-playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "20.7.0" } google-play-core = { module = "com.google.android.play:core", version = "1.10.3" } google-play-core-ktx = { module = "com.google.android.play:core-ktx", version = "1.8.1" } -firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version = "18.6.1" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version = "18.6.2" } firebase-crashlytics-gradle-plugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.9" } # AndroidX @@ -133,7 +133,7 @@ detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plu detekt-ruleset-compiler = { module = "com.braisgabin.detekt:kotlin-compiler-wrapper", version = "0.0.4" } detekt-ruleset-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-ruleset-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.3.11" } -detekt-ruleset-ivy-explicit = { module = "com.github.Ivy-Apps:detekt-explicit", version = "v0.0.5" } +detekt-ruleset-ivy-explicit = { module = "com.github.Ivy-Apps:detekt-explicit", version = "v0.0.7" } slack-lint-compose = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.3.1" } # KSP From 4e93f9d1ea90dd05d24e624cc12b98f9a3776fda Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Thu, 15 Feb 2024 14:58:07 +0200 Subject: [PATCH 05/10] Update CI-Troubleshooting.md - link Compose stability articles --- docs/CI-Troubleshooting.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/CI-Troubleshooting.md b/docs/CI-Troubleshooting.md index 674f8c23a5..b3dd852979 100644 --- a/docs/CI-Troubleshooting.md +++ b/docs/CI-Troubleshooting.md @@ -58,7 +58,11 @@ If this job is failing this means that your changes break an existing unit test. ## Compose Stability -This GitHub Action checks whether your `@Composable` functions are stable (i.e. "restartable" and "skippable"). If it fails it means that some of your composables are unstable. That causes unnecessary recompositions which can lead to lost frames and laggy UI/UX especially when animation or scrolling. You must fix that! To fix it, open the failing working and see the output from report - it tells you which `@Composable` functions are unstable and what parameters cause that. +This GitHub Action checks whether your `@Composable` functions are stable (i.e. "restartable" and "skippable"). If it fails it means that some of your composables are unstable. That causes unnecessary recompositions which can lead to lost frames and laggy UI/UX especially when animation or scrolling. You must fix that! To fix it, open the failing working and see the output from the report - it tells you which `@Composable` functions are unstable and what parameters cause that. + +**Fixing Stability issues:** +1. Read https://developer.android.com/jetpack/compose/performance/stability/fix +2. https://developer.android.com/jetpack/compose/performance/stability **Compose Stability baseline** (not recommended) ``` From 8b0b093f04b0fd615b438d392eee6a62d8bc901c Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Fri, 16 Feb 2024 23:08:43 +0200 Subject: [PATCH 06/10] Attempt to fix the "Demo" APK being slow by making it not debuggable --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0e85fc3f3b..63aadb7637 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,7 +82,7 @@ android { matchingFallbacks.add("release") - isDebuggable = true + isDebuggable = false isDefault = false signingConfig = signingConfigs.getByName("debug") From 9f480691260a97b42f61faa55bacdd252adb4975 Mon Sep 17 00:00:00 2001 From: Hussien Fahmy Date: Sat, 17 Feb 2024 00:03:50 +0200 Subject: [PATCH 07/10] Exclude future planned payments that aren't paid from being calculated in ReportViewModel (#2970) --- .../reports/src/main/java/com/ivy/reports/ReportViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 f039061ac5..cb42894543 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt @@ -271,8 +271,7 @@ class ReportViewModel @Inject constructor( filterRange ?: return@filter false - (it.dateTime != null && filterRange.includes(it.dateTime!!)) || - (it.dueDate != null && filterRange.includes(it.dueDate!!)) + it.dateTime != null && filterRange.includes(it.dateTime!!) } .filter { trn -> // Filter by Accounts From aadfa523665f6d0b2d3f698fd48109f400128973 Mon Sep 17 00:00:00 2001 From: Ahmed Haroun Date: Sat, 17 Feb 2024 04:35:15 -0500 Subject: [PATCH 08/10] Removed composables from unstable composables baseline (#2967) * Removed composables from unstable composables baseline * Made the composable function IconsRow() paramaters stable * Revert "Made the composable function IconsRow() paramaters stable" This reverts commit 2a93c7284fcf1e316b33360b693ec8ccec195877. * Marked ComposeViewModel as Stable * Marked All Implementations of ComposeViewModel as Stable * Made IconsRow() paramaters stable * Made SortModel()'s parameters stable * Made PieChart()'s parameters stable * Added composables to unstable composables baseline --- .../src/main/java/com/ivy/accounts/AccountsViewModel.kt | 2 ++ .../main/java/com/ivy/attributions/AttributionsViewModel.kt | 2 ++ .../src/main/java/com/ivy/balance/BalanceViewModel.kt | 2 ++ .../src/main/java/com/ivy/budgets/BudgetViewModel.kt | 2 ++ .../src/main/java/com/ivy/categories/CategoriesScreen.kt | 3 ++- .../src/main/java/com/ivy/categories/CategoriesViewModel.kt | 2 ++ .../main/java/com/ivy/contributors/ContributorsViewModel.kt | 2 ++ .../java/com/ivy/transaction/EditTransactionViewModel.kt | 2 ++ .../java/com/ivy/exchangerates/ExchangeRatesViewModel.kt | 2 ++ .../src/main/java/com/ivy/features/FeaturesViewModel.kt | 2 ++ screen/home/src/main/java/com/ivy/home/HomeViewModel.kt | 2 ++ .../loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt | 2 ++ .../java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt | 2 ++ .../com/ivy/onboarding/viewmodel/OnboardingViewModel.kt | 2 ++ screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt | 6 ++++-- .../java/com/ivy/piechart/PieChartStatisticViewModel.kt | 2 ++ .../main/java/com/ivy/planned/edit/EditPlannedViewModel.kt | 2 ++ .../java/com/ivy/planned/list/PlannedPaymentsViewModel.kt | 2 ++ .../src/main/java/com/ivy/releases/ReleasesViewModel.kt | 2 ++ .../src/main/java/com/ivy/reports/ReportViewModel.kt | 2 ++ .../search/src/main/java/com/ivy/search/SearchViewModel.kt | 2 ++ .../src/main/java/com/ivy/settings/SettingsViewModel.kt | 2 ++ .../main/java/com/ivy/transactions/TransactionsViewModel.kt | 2 ++ .../domain/src/main/java/com/ivy/domain/ComposeViewModel.kt | 2 ++ .../com/ivy/legacy/legacy/ui/theme/modal/ChooseIconModal.kt | 6 +++++- 25 files changed, 55 insertions(+), 4 deletions(-) diff --git a/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt b/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt index 252d58c2e2..c3e7ac8d0e 100644 --- a/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt +++ b/screen/accounts/src/main/java/com/ivy/accounts/AccountsViewModel.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.data.db.dao.write.WriteAccountDao @@ -30,6 +31,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @SuppressLint("StaticFieldLeak") @HiltViewModel class AccountsViewModel @Inject constructor( diff --git a/screen/attributions/src/main/java/com/ivy/attributions/AttributionsViewModel.kt b/screen/attributions/src/main/java/com/ivy/attributions/AttributionsViewModel.kt index 4cacf2120d..5039c453ec 100644 --- a/screen/attributions/src/main/java/com/ivy/attributions/AttributionsViewModel.kt +++ b/screen/attributions/src/main/java/com/ivy/attributions/AttributionsViewModel.kt @@ -1,10 +1,12 @@ package com.ivy.attributions import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import com.ivy.domain.ComposeViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +@Stable @HiltViewModel class AttributionsViewModel @Inject constructor( private val attributionsProvider: AttributionsProvider diff --git a/screen/balance/src/main/java/com/ivy/balance/BalanceViewModel.kt b/screen/balance/src/main/java/com/ivy/balance/BalanceViewModel.kt index 9c88dd67a6..0781c7322c 100644 --- a/screen/balance/src/main/java/com/ivy/balance/BalanceViewModel.kt +++ b/screen/balance/src/main/java/com/ivy/balance/BalanceViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.balance import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -16,6 +17,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class BalanceViewModel @Inject constructor( private val plannedPaymentsLogic: PlannedPaymentsLogic, diff --git a/screen/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt b/screen/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt index 455d2e3246..7ea9bf97b5 100644 --- a/screen/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt +++ b/screen/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.budgets import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -35,6 +36,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class BudgetViewModel @Inject constructor( private val sharedPrefs: SharedPrefs, 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 ad30aab6a6..1d64113d59 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesScreen.kt @@ -69,6 +69,7 @@ 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 com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.util.UUID @@ -415,7 +416,7 @@ private fun CategoryHeader( @Suppress("UnusedParameter") @Composable fun BoxWithConstraintsScope.SortModal( - items: List, + items: ImmutableList, visible: Boolean, initialType: SortOrder, dismiss: () -> Unit, diff --git a/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt b/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt index 51fda3d7d5..552da7d208 100644 --- a/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt +++ b/screen/categories/src/main/java/com/ivy/categories/CategoriesViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.categories import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.Transaction @@ -31,6 +32,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class CategoriesViewModel @Inject constructor( private val categoryCreator: CategoryCreator, diff --git a/screen/contributors/src/main/java/com/ivy/contributors/ContributorsViewModel.kt b/screen/contributors/src/main/java/com/ivy/contributors/ContributorsViewModel.kt index a0d3720e37..6eeb1cec56 100644 --- a/screen/contributors/src/main/java/com/ivy/contributors/ContributorsViewModel.kt +++ b/screen/contributors/src/main/java/com/ivy/contributors/ContributorsViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.contributors import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.domain.ComposeViewModel @@ -10,6 +11,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class ContributorsViewModel @Inject constructor( private val ivyWalletRepositoryDataSource: IvyWalletRepositoryDataSource 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 0e6e580859..73c785dbe2 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 @@ -1,6 +1,7 @@ package com.ivy.transaction import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -58,6 +59,7 @@ import java.time.LocalTime import java.util.UUID import javax.inject.Inject +@Stable @HiltViewModel class EditTransactionViewModel @Inject constructor( private val loanDao: LoanDao, diff --git a/screen/exchange-rates/src/main/java/com/ivy/exchangerates/ExchangeRatesViewModel.kt b/screen/exchange-rates/src/main/java/com/ivy/exchangerates/ExchangeRatesViewModel.kt index 76a9e89add..8c8355b1a3 100644 --- a/screen/exchange-rates/src/main/java/com/ivy/exchangerates/ExchangeRatesViewModel.kt +++ b/screen/exchange-rates/src/main/java/com/ivy/exchangerates/ExchangeRatesViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.exchangerates import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.data.db.dao.read.ExchangeRatesDao @@ -22,6 +23,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject +@Stable @HiltViewModel class ExchangeRatesViewModel @Inject constructor( private val exchangeRatesDao: ExchangeRatesDao, diff --git a/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt b/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt index 909fad9a70..088004c966 100644 --- a/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt +++ b/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt @@ -3,6 +3,7 @@ package com.ivy.features import android.annotation.SuppressLint import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.lifecycle.viewModelScope import com.ivy.domain.ComposeViewModel import com.ivy.domain.features.Features @@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @SuppressLint("StaticFieldLeak") @HiltViewModel class FeaturesViewModel @Inject constructor( diff --git a/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt b/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt index 93baec987b..6cd77663a3 100644 --- a/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt +++ b/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.home import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.Theme @@ -56,6 +57,7 @@ import kotlinx.coroutines.launch import java.math.BigDecimal import javax.inject.Inject +@Stable @HiltViewModel class HomeViewModel @Inject constructor( private val ivyContext: IvyWalletCtx, diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt index ce35787af0..766fead024 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.loans.loan import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.SharedPrefs @@ -37,6 +38,7 @@ import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject +@Stable @HiltViewModel class LoanViewModel @Inject constructor( private val loanRecordDao: LoanRecordDao, diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt index 57d7a5caf0..8d63d5baca 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.loans.loandetails import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -49,6 +50,7 @@ import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject +@Stable @HiltViewModel class LoanDetailsViewModel @Inject constructor( private val loanDao: LoanDao, 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 5c315fef00..8b163a8f19 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 @@ -1,6 +1,7 @@ package com.ivy.onboarding.viewmodel import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -43,6 +44,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Stable @HiltViewModel class OnboardingViewModel @Inject constructor( private val ivyContext: IvyWalletCtx, 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 546d908228..0e8c4c45e4 100644 --- a/screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt +++ b/screen/piechart/src/main/java/com/ivy/piechart/PieChart.kt @@ -28,6 +28,8 @@ import com.ivy.legacy.utils.timeNowUTC import com.ivy.resources.R import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.IvyIcon +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import timber.log.Timber import kotlin.math.acos import kotlin.math.sqrt @@ -38,7 +40,7 @@ const val RADIUS_DP = 112f @Composable fun PieChart( type: TransactionType, - categoryAmounts: List, + categoryAmounts: ImmutableList, selectedCategory: SelectedCategory?, modifier: Modifier = Modifier, onCategoryClicked: (Category?) -> Unit = {} @@ -270,7 +272,7 @@ private fun Preview() { PieChart( type = TransactionType.EXPENSE, - categoryAmounts = listOf( + categoryAmounts = persistentListOf( CategoryAmount( category = Category("Bills", Green.toArgb()), amount = 791.0 diff --git a/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticViewModel.kt b/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticViewModel.kt index 5243e84f2d..9b023b262c 100644 --- a/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticViewModel.kt +++ b/screen/piechart/src/main/java/com/ivy/piechart/PieChartStatisticViewModel.kt @@ -1,6 +1,7 @@ package com.ivy.piechart import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -25,6 +26,7 @@ import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject +@Stable @HiltViewModel class PieChartStatisticViewModel @Inject constructor( private val settingsDao: SettingsDao, 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 4108863971..337666b0ad 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 @@ -1,6 +1,7 @@ package com.ivy.planned.edit import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -39,6 +40,7 @@ import kotlinx.coroutines.launch import java.time.LocalDateTime import javax.inject.Inject +@Stable @HiltViewModel class EditPlannedViewModel @Inject constructor( private val accountDao: AccountDao, diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt b/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt index a7e366daeb..00bc7361d1 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/list/PlannedPaymentsViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.planned.list import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -21,6 +22,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class PlannedPaymentsViewModel @Inject constructor( private val settingsDao: SettingsDao, diff --git a/screen/releases/src/main/java/com/ivy/releases/ReleasesViewModel.kt b/screen/releases/src/main/java/com/ivy/releases/ReleasesViewModel.kt index a46bc83628..b0698e29ea 100644 --- a/screen/releases/src/main/java/com/ivy/releases/ReleasesViewModel.kt +++ b/screen/releases/src/main/java/com/ivy/releases/ReleasesViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.releases import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.domain.ComposeViewModel @@ -10,6 +11,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class ReleasesViewModel @Inject constructor( private val releasesDataSource: ReleasesDataSource, 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 cb42894543..bf4c526764 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt @@ -3,6 +3,7 @@ package com.ivy.reports import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.toArgb @@ -49,6 +50,7 @@ import java.math.BigDecimal import java.util.UUID import javax.inject.Inject +@Stable @HiltViewModel class ReportViewModel @Inject constructor( private val plannedPaymentsLogic: PlannedPaymentsLogic, diff --git a/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt b/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt index 524687502e..f7ea79df9f 100644 --- a/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt +++ b/screen/search/src/main/java/com/ivy/search/SearchViewModel.kt @@ -2,6 +2,7 @@ package com.ivy.search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.TransactionHistoryItem @@ -22,6 +23,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @HiltViewModel class SearchViewModel @Inject constructor( private val trnsWithDateDivsAct: TrnsWithDateDivsAct, 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 b6796318a7..b74935f566 100644 --- a/screen/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt +++ b/screen/settings/src/main/java/com/ivy/settings/SettingsViewModel.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope @@ -35,6 +36,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject +@Stable @SuppressLint("StaticFieldLeak") @HiltViewModel class SettingsViewModel @Inject constructor( 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 7cfda4aaa4..36e53d54c6 100644 --- a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt +++ b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt @@ -1,6 +1,7 @@ package com.ivy.transactions import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.toArgb @@ -59,6 +60,7 @@ import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject +@Stable @HiltViewModel class TransactionsViewModel @Inject constructor( private val accountDao: AccountDao, diff --git a/shared/domain/src/main/java/com/ivy/domain/ComposeViewModel.kt b/shared/domain/src/main/java/com/ivy/domain/ComposeViewModel.kt index b2187cfaae..138096224a 100644 --- a/shared/domain/src/main/java/com/ivy/domain/ComposeViewModel.kt +++ b/shared/domain/src/main/java/com/ivy/domain/ComposeViewModel.kt @@ -1,11 +1,13 @@ package com.ivy.domain import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel /** * A simple base ViewModel utilizing Compose' reactivity. */ +@Stable abstract class ComposeViewModel : ViewModel() { /** * Optimized for Compose ui state. diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/ChooseIconModal.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/ChooseIconModal.kt index 68ac1a72b3..164afcf7b8 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/ChooseIconModal.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/ChooseIconModal.kt @@ -41,6 +41,8 @@ import com.ivy.resources.R import com.ivy.wallet.ui.theme.Ivy import com.ivy.wallet.ui.theme.components.ItemIconS import com.ivy.wallet.ui.theme.dynamicContrast +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import java.util.UUID @Deprecated("Old design system. Use `:ivy-design` and Material3") @@ -219,6 +221,8 @@ private fun LazyListScope.addIconsRowIfNotEmpty( onIconSelected: (String) -> Unit ) { + val rowAcc = rowAcc.toImmutableList() + if (rowAcc.isNotEmpty()) { item { IconsRow( @@ -236,7 +240,7 @@ private fun LazyListScope.addIconsRowIfNotEmpty( @Composable private fun IconsRow( - icons: List, + icons: ImmutableList, selectedIcon: String?, color: Color, From 18e4cf06525e5f5dc89afb9c6dcfc3633bb8d55b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:35:18 +0200 Subject: [PATCH 09/10] Bump the ivy-wallet group with 7 updates (#2972) Bumps the ivy-wallet group with 7 updates: | Package | From | To | | --- | --- | --- | | [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | [org.jetbrains.kotlinx:kotlinx-coroutines-play-services](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | [org.jetbrains.kotlinx:kotlinx-coroutines-play-services](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) | `1.6.2` | `1.6.3` | | [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) | `1.7.3` | `1.8.0` | | com.google.android.gms:play-services-auth | `20.7.0` | `21.0.0` | | [org.jetbrains.kotlinx.kover](https://github.com/Kotlin/kotlinx-kover) | `0.7.5` | `0.7.6` | Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-android` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-play-services` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-android` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-play-services` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `org.jetbrains.kotlinx:kotlinx-serialization-json` from 1.6.2 to 1.6.3 - [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases) - [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.6.2...v1.6.3) Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0) Updates `com.google.android.gms:play-services-auth` from 20.7.0 to 21.0.0 Updates `org.jetbrains.kotlinx.kover` from 0.7.5 to 0.7.6 - [Release notes](https://github.com/Kotlin/kotlinx-kover/releases) - [Changelog](https://github.com/Kotlin/kotlinx-kover/blob/main/CHANGELOG.md) - [Commits](https://github.com/Kotlin/kotlinx-kover/compare/v0.7.5...v0.7.6) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-play-services dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-play-services dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ivy-wallet - dependency-name: com.google.android.gms:play-services-auth dependency-type: direct:production update-type: version-update:semver-major dependency-group: ivy-wallet - dependency-name: org.jetbrains.kotlinx.kover dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ivy-wallet ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cff155de33..8df650c9b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "1.9.22" -kotlin-coroutines = "1.7.3" +kotlin-coroutines = "1.8.0" ktor = "2.3.8" arrow = "1.2.1" kotest = "5.8.0" @@ -38,7 +38,7 @@ kotlin-coroutines-googleplay-temp = { module = "org.jetbrains.kotlinx:kotlinx-co kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-collections" } # KotlinX Serialization -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.2" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" } kotlinx-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } # Ktor @@ -102,7 +102,7 @@ hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", # Google google-services-plugin = { module = "com.google.gms:google-services", version = "4.4.1" } -google-playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "20.7.0" } +google-playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "21.0.0" } google-play-core = { module = "com.google.android.play:core", version = "1.10.3" } google-play-core-ktx = { module = "com.google.android.play:core-ktx", version = "1.8.1" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version = "18.6.2" } @@ -223,4 +223,4 @@ opencsv = [ [plugins] gradleWrapperUpgrade = { id = "org.gradle.wrapper-upgrade", version = "0.11.4" } -koverPlugin = { id = "org.jetbrains.kotlinx.kover", version = "0.7.5" } \ No newline at end of file +koverPlugin = { id = "org.jetbrains.kotlinx.kover", version = "0.7.6" } \ No newline at end of file From bd7b890a9415dc58b77a0bb9196c5ede13952a13 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Tue, 20 Feb 2024 01:48:01 +0200 Subject: [PATCH 10/10] Update YouTube-Videos.md --- docs/resources/YouTube-Videos.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/resources/YouTube-Videos.md b/docs/resources/YouTube-Videos.md index 2c71a75310..d753c91914 100644 --- a/docs/resources/YouTube-Videos.md +++ b/docs/resources/YouTube-Videos.md @@ -12,6 +12,10 @@ - [Domain Modeling Made Functional](https://youtu.be/2JB1_e5wZmU) [Scott Wlaschin] - [The Grug Brained Developer - reaction](https://www.youtube.com/watch?v=0KFiDK9r4UI) [ThePrimeTime] +## Math + +- [Categories for the Working Hacker](https://youtu.be/gui_SE8rJUM?si=nBHmCInfNI65q2x7) [Philip Wadler] + ## Kotlin Flows - [Kotlin Flows in Practice](https://www.youtube.com/watch?v=fSB6_KE95bU) [Android Developers]