Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a0b0efe
approval screen in porgress
ufuomaisaac Jan 19, 2026
fb9ae31
still in progress but app crashes when the approve button is clicked
ufuomaisaac Jan 19, 2026
fedeb20
still in progress
ufuomaisaac Jan 21, 2026
fdf087e
added a new date format
ufuomaisaac Jan 21, 2026
bae17c5
fixed ExceptionInInitializerError issue
ufuomaisaac Jan 22, 2026
c972983
approve account functionality now working correctly
ufuomaisaac Jan 22, 2026
1f4a960
code clean up
ufuomaisaac Jan 22, 2026
f5af88d
ran the necessary ./gradlew checks
ufuomaisaac Jan 22, 2026
711227e
code cleanup
ufuomaisaac Jan 22, 2026
c49b501
Merge branch 'development' into issue
ufuomaisaac Jan 23, 2026
13cf1e4
made the necessary changes after review
ufuomaisaac Jan 28, 2026
7a9cfc8
Merge branch 'issue' of github.com:ufuomaisaac/android-client into issue
ufuomaisaac Jan 28, 2026
167502c
code clean up
ufuomaisaac Jan 28, 2026
e65fbbc
Merge branch 'development' into issue
ufuomaisaac Jan 28, 2026
69be531
code clean up
ufuomaisaac Jan 28, 2026
e145469
Merge branch 'issue' of github.com:ufuomaisaac/android-client into issue
ufuomaisaac Jan 28, 2026
e7b3ebb
code clean up
ufuomaisaac Jan 28, 2026
e5e9240
Improved the UI
ufuomaisaac Feb 2, 2026
d544651
Merge branch 'development' into issue
ufuomaisaac Feb 3, 2026
70b7ab0
resolved changes suggested by coderabbit
ufuomaisaac Feb 3, 2026
83cfda6
ran gradle checks
ufuomaisaac Feb 3, 2026
bedbd25
fix merge conflicts
ufuomaisaac Feb 3, 2026
31f595e
Merge branch 'development' into issue
ufuomaisaac Feb 3, 2026
9642f8a
Merge branch 'development' into issue
ufuomaisaac Feb 3, 2026
380a137
removed used import
ufuomaisaac Feb 3, 2026
7bbde81
merged
ufuomaisaac Feb 3, 2026
9c3a9b3
Good
ufuomaisaac Feb 4, 2026
f608a15
resolved potential issue from coderabbit
ufuomaisaac Feb 4, 2026
7ca16f8
refactored recurringDepositModule
ufuomaisaac Feb 6, 2026
9097cdd
fixed requested changes
ufuomaisaac Feb 6, 2026
732f823
Merge branch 'development' into issue
ufuomaisaac Feb 6, 2026
a9d5ffa
Used APIEndPoint.CREATE_RECURRING_DEPOSIT_ACCOUNTS instead of the har…
ufuomaisaac Feb 6, 2026
fb050aa
Merge branch 'issue' of github.com:ufuomaisaac/android-client into issue
ufuomaisaac Feb 6, 2026
003d96a
requested changes resolved
ufuomaisaac Feb 9, 2026
aec28c4
good
ufuomaisaac Feb 9, 2026
8e5f350
good
ufuomaisaac Feb 9, 2026
d563680
Merge branch 'development' into issue
ufuomaisaac Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.format
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.Padding
import kotlinx.datetime.format.byUnicodePattern
import kotlinx.datetime.format.char
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
Expand All @@ -40,6 +43,14 @@ object DateHelper {

const val MONTH_FORMAT = "dd MMMM"

private val apiDateFormat = LocalDateTime.Format {
dayOfMonth(Padding.ZERO)
char(' ')
monthName(MonthNames.ENGLISH_FULL)
char(' ')
year()
}

private val fullMonthFormat = LocalDateTime.Format {
byUnicodePattern(FULL_MONTH)
}
Expand Down Expand Up @@ -451,4 +462,12 @@ object DateHelper {
null
}
}

@OptIn(ExperimentalTime::class)
fun getDateAsStringForApproval(timeInMillis: Long): String {
val instant = Instant.fromEpochMilliseconds(timeInMillis)
.toLocalDateTime(TimeZone.currentSystemDefault())

return instant.format(apiDateFormat) // Returns "21 January 2026"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.mifos.core.data.repository

import com.mifos.core.common.utils.DataState
import com.mifos.core.model.objects.payloads.RecurringDepositAccountPayload
import com.mifos.core.model.objects.template.recurring.approval.RecurringDepositApproval
import com.mifos.core.network.GenericResponse
import com.mifos.room.entities.templates.recurringDeposit.RecurringDepositAccountTemplate
import kotlinx.coroutines.flow.Flow
Expand All @@ -25,4 +26,9 @@ interface RecurringAccountRepository {
fun createRecurringDepositAccount(
recurringDepositAccountPayload: RecurringDepositAccountPayload?,
): Flow<DataState<GenericResponse>>

suspend fun approveRecurringDepositAccount(
accountId: String,
approval: RecurringDepositApproval,
): GenericResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.mifos.core.common.utils.DataState
import com.mifos.core.common.utils.asDataStateFlow
import com.mifos.core.data.repository.RecurringAccountRepository
import com.mifos.core.model.objects.payloads.RecurringDepositAccountPayload
import com.mifos.core.model.objects.template.recurring.approval.RecurringDepositApproval
import com.mifos.core.network.GenericResponse
import com.mifos.core.network.datamanager.DataManagerRecurringAccount
import com.mifos.room.entities.templates.recurringDeposit.RecurringDepositAccountTemplate
Expand All @@ -39,4 +40,14 @@ class RecurringAccountRepositoryImp(
recurringDepositAccountPayload,
).asDataStateFlow()
}

override suspend fun approveRecurringDepositAccount(
accountId: String,
approval: RecurringDepositApproval,
): GenericResponse {
return dataManagerRecurringAccount.approveRecurringDepositAccount(
accountId,
approval,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.mifos.core.domain.useCases.AddClientPinpointLocationUseCase
import com.mifos.core.domain.useCases.AddDataTableEntryUseCase
import com.mifos.core.domain.useCases.AddNoteUseCase
import com.mifos.core.domain.useCases.ApproveCheckerUseCase
import com.mifos.core.domain.useCases.ApproveRecurringDepositUseCase
import com.mifos.core.domain.useCases.ApproveSavingsApplicationUseCase
import com.mifos.core.domain.useCases.CreateChargesUseCase
import com.mifos.core.domain.useCases.CreateClientIdentifierUseCase
Expand Down Expand Up @@ -171,4 +172,5 @@ val UseCaseModule = module {
factoryOf(::DeleteNoteUseCase)
factoryOf(::CreateSignatureUseCase)
factoryOf(::UpdateSignatureUseCase)
factoryOf(::ApproveRecurringDepositUseCase)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2026 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/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.domain.useCases

import com.mifos.core.common.utils.DataState
import com.mifos.core.common.utils.asDataStateFlow
import com.mifos.core.data.repository.RecurringAccountRepository
import com.mifos.core.model.objects.template.recurring.approval.RecurringDepositApproval
import com.mifos.core.network.GenericResponse
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class ApproveRecurringDepositUseCase(
private val repository: RecurringAccountRepository,
) {
operator fun invoke(
accountId: String,
approval: RecurringDepositApproval,
): Flow<DataState<GenericResponse>> {
return flow {
emit(repository.approveRecurringDepositAccount(accountId, approval))
}.asDataStateFlow()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2026 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/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.model.objects.template.recurring.approval

import kotlinx.serialization.Serializable

@Serializable
data class RecurringDepositApproval(
var locale: String = "en",

var dateFormat: String = "dd MMMM yyyy",

var approvedOnDate: String? = null,

var note: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ actual val KtorHttpClient: HttpClient
install(ContentNegotiation) {
json(
Json {
encodeDefaults = true
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.mifos.core.network.datamanager

import com.mifos.core.common.utils.extractErrorMessage
import com.mifos.core.model.objects.payloads.RecurringDepositAccountPayload
import com.mifos.core.model.objects.template.recurring.approval.RecurringDepositApproval
import com.mifos.core.network.BaseApiManager
import com.mifos.core.network.GenericResponse
import com.mifos.room.entities.templates.recurringDeposit.RecurringDepositAccountTemplate
Expand Down Expand Up @@ -51,4 +52,21 @@ class DataManagerRecurringAccount(
productId,
)
}

suspend fun approveRecurringDepositAccount(
accountId: String,
approval: RecurringDepositApproval,
): GenericResponse {
val response = mBaseApiManager.recurringSavingsAccountService.approveRecurringDepositAccount(
accountId,
approval,
)
if (!response.status.isSuccess()) {
val errorMessage = extractErrorMessage(response)
throw IllegalStateException(errorMessage)
}

val json = Json { ignoreUnknownKeys = true }
return json.decodeFromString<GenericResponse>(response.bodyAsText())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
package com.mifos.core.network.services

import com.mifos.core.model.objects.payloads.RecurringDepositAccountPayload
import com.mifos.core.model.objects.template.recurring.approval.RecurringDepositApproval
import com.mifos.room.basemodel.APIEndPoint
import com.mifos.room.entities.templates.recurringDeposit.RecurringDepositAccountTemplate
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
Expand All @@ -31,4 +33,10 @@ interface RecurringAccountService {
@Query("clientId") clientId: Int,
@Query("productId") productId: Int?,
): Flow<RecurringDepositAccountTemplate>

@POST(APIEndPoint.CREATE_RECURRING_DEPOSIT_ACCOUNTS + "/{accountId}?command=approve")
suspend fun approveRecurringDepositAccount(
@Path("accountId") accountId: String,
@Body approval: RecurringDepositApproval,
): HttpResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ import com.mifos.feature.note.navigation.noteDestination
import com.mifos.feature.note.notes.navigateToNoteScreen
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.navigateToRecurringAccountRoute
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.recurringAccountDestination
import com.mifos.feature.recurringDeposit.recurringAccountApproval.navigateToRecurringDepositAccountApproval
import com.mifos.feature.recurringDeposit.recurringAccountApproval.recurringDepositAccountApprovalDestination
import com.mifos.feature.savings.navigation.navigateToAddSavingsAccount
import com.mifos.feature.savings.navigation.navigateToSavingsAccountApproval
import com.mifos.feature.savings.navigation.navigateToSavingsAccountSummaryScreen
Expand Down Expand Up @@ -264,9 +266,17 @@ fun NavGraphBuilder.clientNavGraph(
clientRecurringDepositAccountDestination(
navController = navController,
navigateBack = navController::popBackStack,
{},
{},
onApproveAccount = { accountId ->
navController.navigateToRecurringDepositAccountApproval(accountId)
},
onViewAccount = {},
)

recurringDepositAccountApprovalDestination(
navigateBack = navController::popBackStack,
navController = navController,
)
Comment on lines 266 to 278
Copy link

@coderabbitai coderabbitai bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for clientRecurringDepositAccountDestination function definition
rg -n "fun.*clientRecurringDepositAccountDestination" --type=kt -A 10

Repository: openMF/android-client

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Search for recurringDepositAccountApprovalDestination and how it uses parameters
rg -n "fun.*recurringDepositAccountApprovalDestination" --type=kt -A 10

Repository: openMF/android-client

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Search for how onApproveAccount callback is defined in the recurring deposit screen
rg -n "onApproveAccount" --type=kt -B 3 -A 3

Repository: openMF/android-client

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for how onViewAccount callback is defined/used
rg -n "onViewAccount" --type=kt -B 3 -A 3

Repository: openMF/android-client

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check the navigateToRecurringDepositAccountApproval extension function
rg -n "navigateToRecurringDepositAccountApproval" --type=kt -A 5

Repository: openMF/android-client

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for clientRecurringDepositAccountDestination function definition
rg -n "fun.*clientRecurringDepositAccountDestination" -A 10

Repository: openMF/android-client

Length of output: 1805


🏁 Script executed:

#!/bin/bash
# Search for recurringDepositAccountApprovalDestination definition
rg -n "fun.*recurringDepositAccountApprovalDestination" -A 10

Repository: openMF/android-client

Length of output: 2090


🏁 Script executed:

#!/bin/bash
# Search for onApproveAccount usage patterns
rg -n "onApproveAccount" -B 2 -A 2

Repository: openMF/android-client

Length of output: 7632


🏁 Script executed:

#!/bin/bash
# Search for onViewAccount usage patterns
rg -n "onViewAccount" -B 2 -A 2

Repository: openMF/android-client

Length of output: 7593


🏁 Script executed:

#!/bin/bash
# Search for navigateToRecurringDepositAccountApproval extension
rg -n "navigateToRecurringDepositAccountApproval" -A 5

Repository: openMF/android-client

Length of output: 2888


Wire the onViewAccount callback and verify the account identifier naming.

The onViewAccount callback is actively used by RecurringDepositAccountScreen (triggered on OnViewAccount event), but it's passed as an empty lambda in the navigation setup. Additionally, the onApproveAccount callback passes accountNumber to navigateToRecurringDepositAccountApproval(), which expects a parameter named accountId—verify these represent the same identifier and use consistent naming throughout.

🤖 Prompt for AI Agents
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt`
around lines 254 - 265, clientRecurringDepositAccountDestination is passing
onViewAccount as an empty lambda and onApproveAccount forwards accountNumber to
navigateToRecurringDepositAccountApproval which expects accountId; wire the
onViewAccount callback to navigate to the appropriate account view (e.g., call
navController.navigateToRecurringDepositAccount with the same identifier) and
make identifier names consistent: either rename accountNumber to accountId in
the onApproveAccount lambda or adapt the
navigateToRecurringDepositAccountApproval invocation to accept accountNumber,
ensuring RecurringDepositAccountScreen's OnViewAccount event and
navigateToRecurringDepositAccountApproval use the same identifier name/type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ufuomaisaac Please address this issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


clientFixedDepositAccountDestination(
navController = navController,
navigateBack = navController::popBackStack,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.mifos.core.common.utils.DateHelper
Expand All @@ -59,7 +64,6 @@ import com.mifos.core.ui.util.EventsEffect
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun RecurringDepositAccountScreen(
navController: NavController,
Expand All @@ -70,6 +74,21 @@ fun RecurringDepositAccountScreen(
viewModel: RecurringDepositAccountViewModel = koinViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val lifecycleOwner = LocalLifecycleOwner.current
val currentAccounts by rememberUpdatedState(state.recurringDepositAccounts)

DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && currentAccounts.isNotEmpty()) {
viewModel.trySendAction(RecurringDepositAccountAction.Refresh)
}
}
lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}

EventsEffect(viewModel.eventFlow) { event ->
when (event) {
Expand Down Expand Up @@ -135,18 +154,16 @@ internal fun RecurringDepositAccountContent(
var expandedIndex by rememberSaveable { mutableStateOf(-1) }

Column(
modifier = modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
) {
MifosBreadcrumbNavBar(navController)

when (state.isLoading) {
true -> MifosProgressIndicator()
false -> {
Column(
Modifier.fillMaxSize()
.padding(
horizontal = DesignToken.padding.large,
horizontal = DesignToken.padding.medium,
),
) {
val notAvailableText = stringResource(Res.string.client_savings_not_avilable)
Expand All @@ -160,7 +177,7 @@ internal fun RecurringDepositAccountContent(
},
)

// todo implement search bar functionality
// TODO: implement search bar functionality
if (state.isSearchBarActive) {
MifosSearchBar(
query = state.searchText,
Expand All @@ -176,7 +193,7 @@ internal fun RecurringDepositAccountContent(
)
}

Spacer(modifier = Modifier.height(DesignToken.padding.largeIncreasedExtra))
Spacer(modifier = Modifier.height(DesignToken.padding.large))

if (state.recurringDepositAccounts.isEmpty()) {
MifosEmptyCard(msg = stringResource(Res.string.client_empty_card_message))
Expand All @@ -186,8 +203,7 @@ internal fun RecurringDepositAccountContent(
MifosActionsSavingsListingComponent(
accountNo = recurringDeposit.accountNo ?: notAvailableText,
savingsProduct = stringResource(Res.string.client_product_recurring_deposit_account),
savingsProductName = recurringDeposit.shortProductName
?: notAvailableText,
savingsProductName = recurringDeposit.shortProductName ?: notAvailableText,
lastActive = if (recurringDeposit.status?.submittedAndPendingApproval == true) {
stringResource(Res.string.client_savings_pending_approval)
} else if (recurringDeposit.lastActiveTransactionDate != null) {
Expand Down Expand Up @@ -215,25 +231,30 @@ internal fun RecurringDepositAccountContent(
Actions.ViewAccount(MifosIcons.Calendar),
)
},
) { actions ->
when (actions) {
is Actions.ViewAccount -> {
onAction(
RecurringDepositAccountAction.ViewAccount(
recurringDeposit.accountNo ?: "",
),
)
}
onActionClicked = { actions ->
recurringDeposit.accountNo?.let { accountNo ->
when (actions) {
is Actions.ViewAccount -> {
onAction(
RecurringDepositAccountAction.ViewAccount(
accountNo,
),
)
}

is Actions.ApproveAccount -> {
RecurringDepositAccountAction.ApproveAccount(
recurringDeposit.accountNo ?: "",
)
}
is Actions.ApproveAccount -> {
onAction(
RecurringDepositAccountAction.ApproveAccount(
accountNo,
),
)
}

else -> null
}
}
else -> Unit
}
}
},
)

Spacer(modifier = Modifier.height(DesignToken.spacing.small))
}
Expand Down
Loading