diff --git a/STCore/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/managers/TransferManager.kt b/STCore/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/managers/TransferManager.kt index caf03625..34866047 100644 --- a/STCore/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/managers/TransferManager.kt +++ b/STCore/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/managers/TransferManager.kt @@ -188,6 +188,26 @@ class TransferManager internal constructor( transferController.getTransfersCountFlow().catchTransfersDbExceptions(crashReport).onEmpty { emit(0L) }, ) { countInRoom, countInRealm -> countInRoom > 0 || countInRealm > 0L }.distinctUntilChanged() + /** + * Emits 'true' if there is at least one sent transfer for the current account, among all organizations of this account. + * + * Unlike [hasAnyTransferFlow], transfers belonging to another account and received transfers are ignored here. + */ + fun hasAccountTransferFlow(): Flow = userDependentFlow( + flowForAuthUser = { userId, _ -> + transferDao.accountTransfersCountFlow(userId, direction = TransferDirection.SENT).map { it > 0 } + }, + flowForGuestUser = { + combine( + transferDao.accountTransfersCountFlow(userId = GuestUser.id, direction = TransferDirection.SENT), + transferController.getTransfersCountFlow(TransferDirection.SENT) + .catchTransfersDbExceptions(crashReport) + .onEmpty { emit(0L) }, + ) { countInRoom, countInRealm -> countInRoom > 0 || countInRealm > 0L } + }, + merge = { hasAuthTransfer, hasGuestTransfer -> hasAuthTransfer || hasGuestTransfer }, + ).distinctUntilChanged().catchTransfersDbExceptions(crashReport) + @OptIn(ExperimentalCoroutinesApi::class) fun getTransferFlow(transferUUID: String): Flow = userDependentFlow( flowForAuthUser = { userId, _ -> diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt index c7e729ec..dc604b0a 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt @@ -36,10 +36,10 @@ import kotlin.time.ExperimentalTime @Dao interface TransferDao { - @Query("SELECT * FROM TransferDB WHERE userOwnerId=:userId AND transferStatus!=:uploadStatus ORDER BY createdAt DESC") + @Query("SELECT * FROM TransferDB WHERE userOwnerId=:userId AND transferStatus!=:excludedUploadStatus ORDER BY createdAt DESC") fun transfersFlow( userId: Long, - uploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, + excludedUploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, ): Flow> @get:Query("SELECT * FROM TransferDB ORDER BY createdAt DESC") @@ -51,31 +51,41 @@ interface TransferDao { @OptIn(ExperimentalTime::class) @Query( """SELECT * FROM TransferDB - WHERE userOwnerId=:userId AND transferStatus!=:uploadStatus AND transferDirection=:direction AND expiresAt >= :currentTime + WHERE userOwnerId=:userId AND transferStatus!=:excludedUploadStatus AND transferDirection=:direction AND expiresAt >= :currentTime AND (organizationAccountId=:organizationAccountId OR (:organizationAccountId IS NULL AND organizationAccountId IS NULL))""" ) fun validTransfersFlow( userId: Long, organizationAccountId: Long?, direction: TransferDirection, - uploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, + excludedUploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, currentTime: Long = Clock.System.now().epochSeconds, ): Flow> @OptIn(ExperimentalTime::class) @Query( """SELECT * FROM TransferDB - WHERE userOwnerId=:userId AND transferStatus!=:uploadStatus AND transferDirection=:direction AND expiresAt < :currentTime + WHERE userOwnerId=:userId AND transferStatus!=:excludedUploadStatus AND transferDirection=:direction AND expiresAt < :currentTime AND (organizationAccountId=:organizationAccountId OR (:organizationAccountId IS NULL AND organizationAccountId IS NULL))""" ) fun expiredTransfersFlow( userId: Long, organizationAccountId: Long?, direction: TransferDirection, - uploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, + excludedUploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, currentTime: Long = Clock.System.now().epochSeconds, ): Flow> + @Query( + """SELECT count(*) FROM TransferDB + WHERE userOwnerId=:userId AND transferStatus!=:excludedUploadStatus AND transferDirection=:direction""" + ) + fun accountTransfersCountFlow( + userId: Long, + direction: TransferDirection, + excludedUploadStatus: TransferStatus = TransferStatus.PENDING_UPLOAD, + ): Flow + @Query("SELECT * FROM TransferDB WHERE userOwnerId=:userId AND id=:transferId LIMIT 1") fun transferFlow(userId: Long, transferId: String): Flow diff --git a/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/v2/TransfersTest.kt b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/v2/TransfersTest.kt index 3ae23a39..846d4048 100644 --- a/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/v2/TransfersTest.kt +++ b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/v2/TransfersTest.kt @@ -40,6 +40,9 @@ class TransfersTest : RobolectricTestsBase() { private lateinit var appDatabase: AppDatabase private val userId = 0L + private val otherUserId = 1L + private val orgA = 10L + private val orgB = 20L private val transferDao get() = appDatabase.getTransferDao() @BeforeTest @@ -124,6 +127,99 @@ class TransfersTest : RobolectricTestsBase() { assertEquals(2, count, "The transfers count must be 2") } + @Test + fun accountTransfersCountFlow_countsTransfersOfAllOrganizationsOfTheAccount() = runTest { + insertTransfer("noOrg", userOwnerId = userId, direction = TransferDirection.SENT) + insertTransfer("orgA1", userOwnerId = userId, direction = TransferDirection.SENT, organizationAccountId = orgA) + insertTransfer("orgA2", userOwnerId = userId, direction = TransferDirection.SENT, organizationAccountId = orgA) + insertTransfer("orgB1", userOwnerId = userId, direction = TransferDirection.SENT, organizationAccountId = orgB) + + val count = transferDao.accountTransfersCountFlow(userId, TransferDirection.SENT).first() + + assertEquals(4, count, "All the transfers of the account must be counted, whatever their organization") + } + + @Test + fun accountTransfersCountFlow_ignoresTransfersOfOtherAccounts() = runTest { + insertTransfer("mine", userOwnerId = userId, direction = TransferDirection.SENT, organizationAccountId = orgA) + insertTransfer("theirs1", userOwnerId = otherUserId, direction = TransferDirection.SENT, organizationAccountId = orgA) + insertTransfer("theirs2", userOwnerId = otherUserId, direction = TransferDirection.SENT) + + assertEquals( + expected = 1, + actual = transferDao.accountTransfersCountFlow(userId, TransferDirection.SENT).first(), + message = "Only the transfers owned by the given user must be counted", + ) + assertEquals( + expected = 2, + actual = transferDao.accountTransfersCountFlow(otherUserId, TransferDirection.SENT).first(), + message = "Each account must count its own transfers", + ) + } + + @Test + fun accountTransfersCountFlow_returnsZero_whenTheAccountHasNoTransfer() = runTest { + insertTransfer("theirs", userOwnerId = otherUserId, direction = TransferDirection.SENT, organizationAccountId = orgA) + + val count = transferDao.accountTransfersCountFlow(userId, TransferDirection.SENT).first() + + assertEquals(0, count, "An account without any transfer must be counted as 0") + } + + @Test + fun accountTransfersCountFlow_filtersByDirection() = runTest { + insertTransfer("sent1", userOwnerId = userId, direction = TransferDirection.SENT) + insertTransfer("sent2", userOwnerId = userId, direction = TransferDirection.SENT, organizationAccountId = orgA) + insertTransfer("received1", userOwnerId = userId, direction = TransferDirection.RECEIVED) + insertTransfer("received2", userOwnerId = userId, direction = TransferDirection.RECEIVED, organizationAccountId = orgA) + insertTransfer("received3", userOwnerId = userId, direction = TransferDirection.RECEIVED, organizationAccountId = orgB) + + assertEquals( + expected = 2, + actual = transferDao.accountTransfersCountFlow(userId, TransferDirection.SENT).first(), + message = "Received transfers must not be counted when asking for sent ones", + ) + assertEquals( + expected = 3, + actual = transferDao.accountTransfersCountFlow(userId, TransferDirection.RECEIVED).first(), + message = "Sent transfers must not be counted when asking for received ones", + ) + } + + @Test + fun accountTransfersCountFlow_excludesPendingUploads() = runTest { + insertTransfer("ready", userOwnerId = userId, direction = TransferDirection.SENT) + insertTransfer( + id = "pendingNoOrg", + userOwnerId = userId, + direction = TransferDirection.SENT, + organizationAccountId = null, + transferStatus = TransferStatus.PENDING_UPLOAD + ) + insertTransfer( + id = "pendingOrgA", + userOwnerId = userId, + direction = TransferDirection.SENT, + organizationAccountId = orgA, + transferStatus = TransferStatus.PENDING_UPLOAD + ) + + assertEquals( + expected = 1, + actual = transferDao.accountTransfersCountFlow(userId, TransferDirection.SENT).first(), + message = "Pending uploads must not be counted, whatever their organization", + ) + assertEquals( + expected = 2, + actual = transferDao.accountTransfersCountFlow( + userId, + TransferDirection.SENT, + excludedUploadStatus = TransferStatus.READY + ).first(), + message = "The excluded status must be the one given as parameter", + ) + } + @Test fun canGetTransferFlow() = runTest { val transfer = DummyTransferForV2.transfer1 @@ -583,6 +679,24 @@ class TransfersTest : RobolectricTestsBase() { } } + private suspend fun insertTransfer( + id: String, + userOwnerId: Long, + direction: TransferDirection, + organizationAccountId: Long? = null, + transferStatus: TransferStatus = TransferStatus.READY, + ) { + transferDao.upsertTransfer( + DummyTransferForV2.notExpired.copy( + id = id, + userOwnerId = userOwnerId, + transferDirection = direction, + organizationAccountId = organizationAccountId, + transferStatus = transferStatus, + ) + ) + } + private suspend fun insertTransfer( transfer: TransferDB, transferDirection: TransferDirection,