Skip to content

Commit 994f895

Browse files
authored
feat(saving): implement savings module with details view, repository, and navigation integration (#42)
1 parent dde08c8 commit 994f895

33 files changed

Lines changed: 948 additions & 457 deletions

File tree

cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2619,6 +2619,44 @@
26192619
| | | +--- org.jetbrains.compose.material:material-icons-extended:1.7.3 (*)
26202620
| | | +--- org.jetbrains.compose.components:components-resources:1.11.0 -> 1.11.1 (*)
26212621
| | | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.11.0 (*)
2622+
| | +--- project :feature:saving
2623+
| | | +--- io.insert-koin:koin-bom:4.2.1 (*)
2624+
| | | +--- io.insert-koin:koin-android:4.2.1 (*)
2625+
| | | +--- io.insert-koin:koin-androidx-compose:4.2.1 (*)
2626+
| | | +--- io.insert-koin:koin-androidx-navigation:4.2.1 (*)
2627+
| | | +--- io.insert-koin:koin-core-viewmodel:4.2.1 (*)
2628+
| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.3.21 (*)
2629+
| | | +--- io.insert-koin:koin-core:4.2.1 (*)
2630+
| | | +--- io.insert-koin:koin-annotations:2.3.1 -> 4.2.1 (*)
2631+
| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0 -> 1.11.0 (*)
2632+
| | | +--- project :core:ui (*)
2633+
| | | +--- project :core-base:ui (*)
2634+
| | | +--- project :core:designsystem (*)
2635+
| | | +--- project :core-base:designsystem (*)
2636+
| | | +--- project :core:data (*)
2637+
| | | +--- project :core:analytics (*)
2638+
| | | +--- io.insert-koin:koin-compose:4.2.1 (*)
2639+
| | | +--- io.insert-koin:koin-compose-viewmodel:4.2.1 (*)
2640+
| | | +--- org.jetbrains.compose.runtime:runtime:1.11.0 -> 1.11.1 (*)
2641+
| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.6 (*)
2642+
| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6 (*)
2643+
| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6 (*)
2644+
| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6 (*)
2645+
| | | +--- org.jetbrains.androidx.savedstate:savedstate:1.4.0 -> 1.3.6 (*)
2646+
| | | +--- org.jetbrains.androidx.core:core-bundle:1.0.1 (*)
2647+
| | | +--- org.jetbrains.androidx.navigation:navigation-compose:2.9.2 (*)
2648+
| | | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0 (*)
2649+
| | | +--- project :core:common (*)
2650+
| | | +--- project :core:domain (*)
2651+
| | | +--- project :core-base:common (*)
2652+
| | | +--- project :core-base:store (*)
2653+
| | | +--- project :core:model (*)
2654+
| | | +--- org.jetbrains.compose.ui:ui:1.11.0 -> 1.11.1 (*)
2655+
| | | +--- org.jetbrains.compose.material3:material3:1.9.0 (*)
2656+
| | | +--- org.jetbrains.compose.foundation:foundation:1.11.0 -> 1.11.1 (*)
2657+
| | | +--- org.jetbrains.compose.material:material-icons-extended:1.7.3 (*)
2658+
| | | +--- org.jetbrains.compose.components:components-resources:1.11.0 -> 1.11.1 (*)
2659+
| | | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.11.0 (*)
26222660
| | +--- org.jetbrains.compose.material3:material3:1.9.0 (*)
26232661
| | +--- org.jetbrains.compose.material:material-icons-extended:1.7.3 (*)
26242662
| | +--- org.jetbrains.compose.foundation:foundation:1.11.0 -> 1.11.1 (*)

cmp-android/dependencies/prodReleaseRuntimeClasspath.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
:feature:auth
2626
:feature:groups
2727
:feature:home
28+
:feature:saving
2829
androidx.activity:activity-compose:1.13.0
2930
androidx.activity:activity-ktx:1.13.0
3031
androidx.activity:activity:1.13.0

cmp-navigation/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ kotlin {
3232
implementation(projects.feature.home)
3333
implementation(projects.feature.auth)
3434
implementation(projects.feature.groups)
35+
implementation(projects.feature.saving)
3536

3637
//put your multiplatform dependencies here
3738
implementation(compose.material3)

cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatedAdminNavBar/AdminNavbarNavigationScreen.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import org.mifos.feature.groups.navigation.groupsNavigationGraph
4343
import org.mifos.feature.home.HomeDestination
4444
import org.mifos.feature.home.homeGraph
4545
import org.mifos.feature.home.navigateToHome
46+
import org.mifos.feature.saving.savingDetails.navigateToSavingDetails
47+
import org.mifos.feature.saving.savingDetails.savingDetailsDestination
4648

4749
@Composable
4850
internal fun AdminNavbarNavigationScreen(
@@ -144,7 +146,13 @@ internal fun AdminNavbarNavigationScreenContent(
144146
popExitTransition = RootTransitionProviders.Kpt.Exit.fadeThrough(motion),
145147
) {
146148
homeGraph()
147-
groupsNavigationGraph(navController = navController)
149+
groupsNavigationGraph(
150+
navController = navController,
151+
onSavingClick = { accountId ->
152+
navController.navigateToSavingDetails(accountId)
153+
},
154+
)
155+
savingDetailsDestination(onBackClick = navController::popBackStack)
148156
}
149157
}
150158
}

cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import org.mifos.core.store.di.appStoreModule
2727
import org.mifos.feature.auth.di.AuthModule
2828
import org.mifos.feature.groups.di.GroupsModule
2929
import org.mifos.feature.home.di.HomeModule
30+
import org.mifos.feature.saving.di.SavingModule
3031

3132
object KoinModules {
3233
private val dataModule = module {
@@ -51,6 +52,7 @@ object KoinModules {
5152
HomeModule,
5253
AuthModule,
5354
GroupsModule,
55+
SavingModule,
5456
)
5557
}
5658

core-base/ui/src/commonTest/kotlin/org/mifos/core/base/ui/freshness/FreshnessIndicatorTest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright 2025 Mifos Initiative
33
*
44
* This Source Code Form is subject to the terms of the Mozilla Public
@@ -14,7 +14,6 @@ import kotlin.test.assertEquals
1414
import kotlin.test.assertTrue
1515
import org.mifos.core.base.store.error.ErrorCategory
1616
import org.mifos.core.base.store.freshness.FreshnessBand
17-
import toLongMessage
1817

1918
/**
2019
* Locks the pure-helper contracts inside `FreshnessIndicator.kt`:

core-base/ui/src/commonTest/kotlin/org/mifos/core/base/ui/screen/DataFreshnessTextTest.kt

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright 2025 Mifos Initiative
33
*
44
* This Source Code Form is subject to the terms of the Mozilla Public
@@ -21,7 +21,7 @@ import kotlin.time.ExperimentalTime
2121
/**
2222
* Verifies the pure helper functions of [mifos.core.base.ui.screen.DataFreshnessIndicator]:
2323
* - [mifos.core.base.ui.screen.formatDurationAgo] human-readable duration formatting
24-
* - [buildStaleText] / [mifos.core.base.ui.screen.buildUpdatingText] timestamp-aware copy
24+
* - [mifos.core.base.ui.screen.buildUpdatingText] timestamp-aware copy
2525
*
2626
* These are the behaviors a user observes in the staleness banner. PLAN-fw-260504
2727
* polish (G2 "always shows just now" / "showing cached data") regressions land
@@ -78,30 +78,6 @@ class DataFreshnessTextTest {
7878
)
7979
}
8080

81-
// ── buildStaleText: indicator copy when offline ───────────────────────
82-
83-
@Test
84-
fun buildStaleText_nullFetchedAt_isPlainOffline_notMisleadingCachedDataMessage() {
85-
// Regression guard: previous copy was "Offline — showing cached data" which
86-
// the user found confusing because (a) it implies we have a timestamp we
87-
// don't, and (b) appears even when serving from cold-restart SoT (the
88-
// common case where lastFetchedAt is null). Plain "Offline" is honest.
89-
assertEquals("Offline", buildStaleText(fetchedAt = null))
90-
}
91-
92-
@Test
93-
fun buildStaleText_recentFetch_showsTimestamp() {
94-
val fiveMinutesAgo = Clock.System.now() - 5.minutes
95-
val text = buildStaleText(fiveMinutesAgo)
96-
assertEquals("Offline · Updated 5m ago", text)
97-
}
98-
99-
@Test
100-
fun buildStaleText_justNow_showsJustNow() {
101-
val tenSecondsAgo = Clock.System.now() - 10.seconds
102-
assertEquals("Offline · Updated just now", buildStaleText(tenSecondsAgo))
103-
}
104-
10581
// ── buildUpdatingText: indicator copy during refresh ──────────────────
10682

10783
@Test

core/data/src/commonMain/kotlin/org/mifos/core/data/di/RepositoryModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import org.mifos.core.data.group.impl.GroupRepositoryImpl
3636
import org.mifos.core.data.infra.NetworkMonitor
3737
import org.mifos.core.data.infra.impl.RoomFetchedAtRepository
3838
import org.mifos.core.data.infra.impl.RoomSubmitOutbox
39+
import org.mifos.core.data.savings.SavingsRepository
40+
import org.mifos.core.data.savings.impl.SavingsRepositoryImpl
3941
import org.mifos.core.data.user.UserDataRepository
4042
import org.mifos.core.data.user.UserLogoutManager
4143
import org.mifos.core.data.user.impl.UserDataRepositoryImpl
@@ -197,6 +199,14 @@ val DataModule = module {
197199
dispatcher = get(),
198200
)
199201
} bind ClientRepository::class
202+
203+
single {
204+
SavingsRepositoryImpl(
205+
dataManager = get(),
206+
networkMonitor = get(),
207+
dispatcher = get(),
208+
)
209+
} bind SavingsRepository::class
200210
}
201211

202212
expect val platformModule: Module
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2026 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
9+
*/
10+
package org.mifos.core.data.mapper.savings
11+
12+
import kotlinx.datetime.LocalDate
13+
import org.mifos.core.model.savings.SavingDetail
14+
import org.mifos.core.model.savings.SavingDetailCurrency
15+
import org.mifos.core.model.savings.SavingDetailStatus
16+
import org.mifos.core.model.savings.SavingDetailSummary
17+
import org.mifos.core.model.savings.SavingDetailTimeline
18+
import org.mifos.core.model.savings.SavingPeriodType
19+
import org.mifos.core.network.fineract.savings.dto.SavingDetailCurrencyDto
20+
import org.mifos.core.network.fineract.savings.dto.SavingDetailDto
21+
import org.mifos.core.network.fineract.savings.dto.SavingDetailStatusDto
22+
import org.mifos.core.network.fineract.savings.dto.SavingDetailSummaryDto
23+
import org.mifos.core.network.fineract.savings.dto.SavingDetailTimelineDto
24+
import org.mifos.core.network.fineract.savings.dto.SavingPeriodTypeDto
25+
26+
fun SavingDetailDto.toModel(): SavingDetail =
27+
SavingDetail(
28+
id = id,
29+
accountNo = accountNo,
30+
clientId = clientId,
31+
clientName = clientName,
32+
savingsProductId = savingsProductId,
33+
savingsProductName = savingsProductName,
34+
fieldOfficerId = fieldOfficerId,
35+
status = status?.toModel(),
36+
timeline = timeline?.toModel(),
37+
currency = currency?.toModel(),
38+
nominalAnnualInterestRate = nominalAnnualInterestRate,
39+
interestCompoundingPeriodType = interestCompoundingPeriodType?.toModel(),
40+
interestPostingPeriodType = interestPostingPeriodType?.toModel(),
41+
interestCalculationType = interestCalculationType?.toModel(),
42+
interestCalculationDaysInYearType = interestCalculationDaysInYearType?.toModel(),
43+
summary = summary?.toModel(),
44+
)
45+
46+
fun SavingDetailStatusDto.toModel(): SavingDetailStatus =
47+
SavingDetailStatus(
48+
id = id,
49+
code = code,
50+
value = value,
51+
submittedAndPendingApproval = submittedAndPendingApproval,
52+
approved = approved,
53+
rejected = rejected,
54+
withdrawnByApplicant = withdrawnByApplicant,
55+
active = active,
56+
closed = closed,
57+
)
58+
59+
fun SavingDetailTimelineDto.toModel(): SavingDetailTimeline =
60+
SavingDetailTimeline(
61+
submittedOnDate = submittedOnDate.toLocalDateOrNull(),
62+
)
63+
64+
fun SavingDetailCurrencyDto.toModel(): SavingDetailCurrency =
65+
SavingDetailCurrency(
66+
code = code,
67+
name = name,
68+
decimalPlaces = decimalPlaces,
69+
displaySymbol = displaySymbol,
70+
nameCode = nameCode,
71+
displayLabel = displayLabel,
72+
)
73+
74+
fun SavingPeriodTypeDto.toModel(): SavingPeriodType =
75+
SavingPeriodType(
76+
id = id,
77+
code = code,
78+
value = value,
79+
)
80+
81+
fun SavingDetailSummaryDto.toModel(): SavingDetailSummary =
82+
SavingDetailSummary(
83+
currency = currency?.toModel(),
84+
accountBalance = accountBalance,
85+
availableBalance = availableBalance,
86+
)
87+
88+
private fun List<Int>?.toLocalDateOrNull(): LocalDate? {
89+
if (this == null || this.size < 3) return null
90+
return try {
91+
LocalDate(this[0], this[1], this[2])
92+
} catch (e: Exception) {
93+
null
94+
}
95+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2026 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
9+
*/
10+
package org.mifos.core.data.savings
11+
12+
import kotlinx.coroutines.flow.Flow
13+
import org.mifos.core.base.store.screen.ScreenState
14+
import org.mifos.core.model.savings.SavingDetail
15+
16+
interface SavingsRepository {
17+
fun getSavingDetails(accountId: Long): Flow<ScreenState<SavingDetail>>
18+
}

0 commit comments

Comments
 (0)