-
Notifications
You must be signed in to change notification settings - Fork 570
feat(pocket): add pocket linked accounts POC #2020
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /* | ||
| * Copyright 2024 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| * | ||
| * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md | ||
| */ | ||
| package org.mifospay.core.data.repository | ||
|
|
||
| import kotlinx.coroutines.flow.Flow | ||
| import org.mifospay.core.common.DataState | ||
| import org.mifospay.core.model.pocket.Pocket | ||
|
|
||
| interface PocketRepository { | ||
| fun getPocket(): Flow<DataState<Pocket>> | ||
| fun linkAccount(accountId: Long, accountType: String): Flow<DataState<Unit>> | ||
| fun delinkAccount(accountId: Long): Flow<DataState<Unit>> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /* | ||
| * Copyright 2024 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| * | ||
| * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md | ||
| */ | ||
| package org.mifospay.core.data.repositoryImpl | ||
|
|
||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.flow | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.onStart | ||
| import kotlinx.coroutines.flow.update | ||
| import org.mifospay.core.common.DataState | ||
| import org.mifospay.core.data.repository.PocketRepository | ||
| import org.mifospay.core.model.pocket.Pocket | ||
| import org.mifospay.core.model.pocket.PocketAccount | ||
| import org.mifospay.core.model.pocket.PocketAccountType | ||
|
|
||
| class DemoPocketRepository : PocketRepository { | ||
| private val pocket = MutableStateFlow( | ||
| Pocket( | ||
| id = 1L, | ||
| clientId = 1L, | ||
| clientName = "Demo Client", | ||
| linkedAccounts = listOf( | ||
| PocketAccount( | ||
| accountId = 1L, | ||
| accountNumber = "000000001", | ||
| accountType = PocketAccountType.SAVINGS, | ||
| balance = 1250.75, | ||
| currency = "USD", | ||
| productName = "Voluntary Savings", | ||
| status = "Active", | ||
| ), | ||
| PocketAccount( | ||
| accountId = 2L, | ||
| accountNumber = "000000002", | ||
| accountType = PocketAccountType.LOAN, | ||
| balance = -420.00, | ||
| currency = "USD", | ||
| productName = "Emergency Loan", | ||
| status = "Active", | ||
| ), | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
| override fun getPocket(): Flow<DataState<Pocket>> = | ||
| .map<Pocket, DataState<Pocket>> { DataState.Success(it) } | ||
| .onStart { emit(DataState.Loading) } | ||
|
|
||
| override fun linkAccount(accountId: Long, accountType: String): Flow<DataState<Unit>> = flow { | ||
| emit(DataState.Loading) | ||
| val type = accountType.toPocketAccountType() | ||
| pocket.update { current -> | ||
| if (current.linkedAccounts.any { it.accountId == accountId }) { | ||
| current | ||
| } else { | ||
| current.copy( | ||
| linkedAccounts = current.linkedAccounts + accountId.toDemoAccount(type), | ||
| ) | ||
| } | ||
| } | ||
| emit(DataState.Success(Unit)) | ||
| } | ||
|
|
||
| override fun delinkAccount(accountId: Long): Flow<DataState<Unit>> = flow { | ||
| emit(DataState.Loading) | ||
| pocket.update { current -> | ||
| current.copy( | ||
| linkedAccounts = current.linkedAccounts.filterNot { it.accountId == accountId }, | ||
| ) | ||
| } | ||
| emit(DataState.Success(Unit)) | ||
| } | ||
| } | ||
|
|
||
| private fun String.toPocketAccountType(): PocketAccountType = | ||
| PocketAccountType.entries.firstOrNull { it.name == uppercase() } ?: PocketAccountType.SAVINGS | ||
|
|
||
| private fun Long.toDemoAccount(accountType: PocketAccountType): PocketAccount { | ||
| val productName = when (accountType) { | ||
| PocketAccountType.SAVINGS -> "Savings Account" | ||
| PocketAccountType.LOAN -> "Loan Account" | ||
| PocketAccountType.SHARE -> "Share Account" | ||
| } | ||
|
|
||
| val balance = when (accountType) { | ||
| PocketAccountType.SAVINGS -> 500.00 + this | ||
| PocketAccountType.LOAN -> -250.00 - this | ||
| PocketAccountType.SHARE -> 100.00 + this | ||
| } | ||
|
|
||
| return PocketAccount( | ||
| accountId = this, | ||
| accountNumber = this.toString().padStart(9, '0'), | ||
| accountType = accountType, | ||
| balance = balance, | ||
| currency = "USD", | ||
| productName = productName, | ||
| status = "Active", | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * Copyright 2024 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| * | ||
| * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md | ||
| */ | ||
| package org.mifospay.core.data.repositoryImpl | ||
|
|
||
| import kotlinx.coroutines.CoroutineDispatcher | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.catch | ||
| import kotlinx.coroutines.flow.flow | ||
| import kotlinx.coroutines.flow.flowOn | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.onStart | ||
| import org.mifospay.core.common.DataState | ||
| import org.mifospay.core.data.repository.PocketRepository | ||
| import org.mifospay.core.model.pocket.Pocket | ||
| import org.mifospay.core.model.pocket.PocketDelinkPayload | ||
| import org.mifospay.core.model.pocket.PocketLinkPayload | ||
| import org.mifospay.core.network.SelfServiceApiManager | ||
|
|
||
| class PocketRepositoryImpl( | ||
| private val apiManager: SelfServiceApiManager, | ||
| private val ioDispatcher: CoroutineDispatcher, | ||
| ) : PocketRepository { | ||
|
|
||
| override fun getPocket(): Flow<DataState<Pocket>> = | ||
| apiManager.pocketApi.getPockets() | ||
| .map<Pocket, DataState<Pocket>> { DataState.Success(it) } | ||
| .onStart { emit(DataState.Loading) } | ||
| .catch { emit(DataState.Error(it)) } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify broad exception catches and absence/presence of cancellation handling.
rg -nP --type=kotlin 'catch\s*\(\s*e:\s*Exception\s*\)' core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/PocketRepositoryImpl.kt
rg -n --type=kotlin 'CancellationException' core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/PocketRepositoryImpl.ktRepository: openMF/mifos-pay Length of output: 132 🏁 Script executed: cat -n core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/PocketRepositoryImpl.ktRepository: openMF/mifos-pay Length of output: 2625 Rethrow Line 35 uses Similarly, lines 43 and 53 should explicitly rethrow Suggested fix+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@
- .catch { emit(DataState.Error(it)) }
+ .catch {
+ if (it is CancellationException) throw it
+ emit(DataState.Error(it))
+ }
@@
- } catch (e: Exception) {
+ } catch (e: Exception) {
+ if (e is CancellationException) throw e
emit(DataState.Error(e))
}
@@
- } catch (e: Exception) {
+ } catch (e: Exception) {
+ if (e is CancellationException) throw e
emit(DataState.Error(e))
}🤖 Prompt for AI Agents |
||
| .flowOn(ioDispatcher) | ||
|
|
||
| override fun linkAccount(accountId: Long, accountType: String): Flow<DataState<Unit>> = flow { | ||
| emit(DataState.Loading) | ||
| try { | ||
| apiManager.pocketApi.linkAccounts(payload = PocketLinkPayload(accountId, accountType)) | ||
| emit(DataState.Success(Unit)) | ||
| } catch (e: Exception) { | ||
| emit(DataState.Error(e)) | ||
| } | ||
| }.flowOn(ioDispatcher) | ||
|
|
||
| override fun delinkAccount(accountId: Long): Flow<DataState<Unit>> = flow { | ||
| emit(DataState.Loading) | ||
| try { | ||
| apiManager.pocketApi.delinkAccounts(payload = PocketDelinkPayload(accountId)) | ||
| emit(DataState.Success(Unit)) | ||
| } catch (e: Exception) { | ||
| emit(DataState.Error(e)) | ||
| } | ||
| }.flowOn(ioDispatcher) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * Copyright 2024 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| * | ||
| * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md | ||
| */ | ||
| package org.mifospay.core.model.pocket | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class Pocket( | ||
| val id: Long, | ||
| val clientId: Long, | ||
| val clientName: String, | ||
| val linkedAccounts: List<PocketAccount> = emptyList(), | ||
| ) { | ||
| val totalBalance: Double | ||
| get() = linkedAccounts.sumOf { it.balance } | ||
| } | ||
|
|
||
| @Serializable | ||
| data class PocketAccount( | ||
| val accountId: Long, | ||
| val accountNumber: String, | ||
| val accountType: PocketAccountType, | ||
| val balance: Double, | ||
| val currency: String, | ||
| val productName: String, | ||
| val status: String, | ||
| ) | ||
|
|
||
| enum class PocketAccountType { | ||
| SAVINGS, | ||
| LOAN, | ||
| SHARE, | ||
| } | ||
|
|
||
| @Serializable | ||
| data class PocketLinkPayload( | ||
| val accountId: Long, | ||
| val accountType: String, | ||
| ) | ||
|
|
||
| @Serializable | ||
| data class PocketDelinkPayload( | ||
| val accountId: Long, | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||
| /* | ||||||||||
| * Copyright 2024 Mifos Initiative | ||||||||||
| * | ||||||||||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||||||||||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||||||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||||||||||
| * | ||||||||||
| * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md | ||||||||||
| */ | ||||||||||
| package org.mifospay.core.network.services | ||||||||||
|
|
||||||||||
| import de.jensklingenberg.ktorfit.http.Body | ||||||||||
| import de.jensklingenberg.ktorfit.http.GET | ||||||||||
| import de.jensklingenberg.ktorfit.http.POST | ||||||||||
| import de.jensklingenberg.ktorfit.http.Query | ||||||||||
| import kotlinx.coroutines.flow.Flow | ||||||||||
| import org.mifospay.core.model.pocket.Pocket | ||||||||||
| import org.mifospay.core.model.pocket.PocketDelinkPayload | ||||||||||
| import org.mifospay.core.model.pocket.PocketLinkPayload | ||||||||||
| import org.mifospay.core.network.utils.ApiEndPoints | ||||||||||
|
|
||||||||||
| interface PocketService { | ||||||||||
| @GET(ApiEndPoints.POCKETS) | ||||||||||
| fun getPockets(): Flow<Pocket> | ||||||||||
|
Comment on lines
+23
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix method name to match return type. The method name 📝 Proposed fix `@GET`(ApiEndPoints.POCKETS)
-fun getPockets(): Flow<Pocket>
+fun getPocket(): Flow<Pocket>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| @POST(ApiEndPoints.POCKETS) | ||||||||||
| suspend fun linkAccounts( | ||||||||||
| @Query("command") command: String = "linkAccounts", | ||||||||||
| @Body payload: PocketLinkPayload, | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| @POST(ApiEndPoints.POCKETS) | ||||||||||
| suspend fun delinkAccounts( | ||||||||||
| @Query("command") command: String = "delinkAccounts", | ||||||||||
| @Body payload: PocketDelinkPayload, | ||||||||||
| ) | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid pushing Pocket Dashboard twice after link success.
This callback navigates to dashboard even though success handling already navigates back from the link screen, which can leave duplicate dashboard entries in back stack.
💡 Suggested fix
linkAccountScreen( navigateBack = navController::navigateUp, - onLinkSuccess = { navController.navigateToPocketDashboard() }, + onLinkSuccess = { /* no-op: LinkAccountScreen already navigates back on success */ }, )📝 Committable suggestion
🤖 Prompt for AI Agents