Skip to content

Commit 9027b37

Browse files
committed
fix(feature:autopay): implement add bill and bill list
1 parent 8b64267 commit 9027b37

File tree

20 files changed

+2495
-47
lines changed

20 files changed

+2495
-47
lines changed

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import org.mifospay.feature.accounts.savingsaccount.details.savingAccountDetailR
2424
import org.mifospay.feature.accounts.savingsaccount.navigateToSavingAccountAddEdit
2525
import org.mifospay.feature.autopay.AutoPayScreen
2626
import org.mifospay.feature.autopay.autoPayGraph
27+
import org.mifospay.feature.autopay.navigateToAddBill
2728
import org.mifospay.feature.autopay.navigateToAddBiller
2829
import org.mifospay.feature.autopay.navigateToAutoPay
2930
import org.mifospay.feature.autopay.navigateToAutoPayHistory
3031
import org.mifospay.feature.autopay.navigateToAutoPayPreferences
3132
import org.mifospay.feature.autopay.navigateToAutoPayRules
3233
import org.mifospay.feature.autopay.navigateToAutoPayScheduleDetails
3334
import org.mifospay.feature.autopay.navigateToAutoPaySetup
35+
import org.mifospay.feature.autopay.navigateToBillList
3436
import org.mifospay.feature.autopay.navigateToBillerList
3537
import org.mifospay.feature.editpassword.navigation.editPasswordScreen
3638
import org.mifospay.feature.editpassword.navigation.navigateToEditPassword
@@ -160,6 +162,12 @@ internal fun MifosNavHost(
160162
onNavigateToBillerList = {
161163
navController.navigateToBillerList()
162164
},
165+
onNavigateToAddBill = {
166+
navController.navigateToAddBill()
167+
},
168+
onNavigateToBillList = {
169+
navController.navigateToBillList()
170+
},
163171
showTopBar = false,
164172
)
165173
},
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2024 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 https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.data.util
11+
12+
import kotlinx.datetime.Clock
13+
import org.mifospay.core.model.autopay.BillFormData
14+
import org.mifospay.core.model.autopay.BillValidationResult
15+
import org.mifospay.core.model.autopay.RecurrencePattern
16+
17+
object BillValidator {
18+
19+
fun validateNameField(name: String): String? {
20+
return when {
21+
name.isBlank() -> "Bill name is required"
22+
name.length < 3 -> "Bill name must be at least 3 characters"
23+
name.length > 100 -> "Bill name must be less than 100 characters"
24+
else -> null
25+
}
26+
}
27+
28+
fun validateAmountField(amount: String): String? {
29+
return when {
30+
amount.isBlank() -> "Amount is required"
31+
!amount.matches(Regex("^\\d+(\\.\\d{1,2})?$")) -> "Please enter a valid amount"
32+
amount.toDoubleOrNull() == null -> "Please enter a valid number"
33+
amount.toDoubleOrNull()!! <= 0 -> "Amount must be greater than 0"
34+
amount.toDoubleOrNull()!! > 999999.99 -> "Amount cannot exceed 999,999.99"
35+
else -> null
36+
}
37+
}
38+
39+
fun validateDueDateField(dueDate: Long): String? {
40+
return when {
41+
dueDate == 0L -> "Due date is required"
42+
dueDate < Clock.System.now().toEpochMilliseconds() -> "Due date cannot be in the past"
43+
else -> null
44+
}
45+
}
46+
47+
fun validateRecurrencePatternField(recurrencePattern: RecurrencePattern): String? {
48+
return when {
49+
recurrencePattern == RecurrencePattern.NONE -> null
50+
else -> null
51+
}
52+
}
53+
54+
fun validateBillerField(billerId: String?, billerName: String?): String? {
55+
return when {
56+
billerId.isNullOrBlank() || billerName.isNullOrBlank() -> "Please select a biller"
57+
else -> null
58+
}
59+
}
60+
61+
fun validateBillForm(formData: BillFormData): BillValidationResult {
62+
val nameError = validateNameField(formData.name)
63+
val amountError = validateAmountField(formData.amount)
64+
val dueDateError = validateDueDateField(formData.dueDate)
65+
val recurrencePatternError = validateRecurrencePatternField(formData.recurrencePattern)
66+
val billerError = validateBillerField(formData.billerId, formData.billerName)
67+
68+
val isValid = nameError == null &&
69+
amountError == null &&
70+
dueDateError == null &&
71+
recurrencePatternError == null &&
72+
billerError == null
73+
74+
return BillValidationResult(
75+
isValid = isValid,
76+
nameError = nameError,
77+
amountError = amountError,
78+
dueDateError = dueDateError,
79+
recurrencePatternError = recurrencePatternError,
80+
billerError = billerError,
81+
)
82+
}
83+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2024 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 https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
11+
12+
package org.mifospay.core.datastore
13+
14+
import com.russhwolf.settings.ExperimentalSettingsApi
15+
import com.russhwolf.settings.Settings
16+
import com.russhwolf.settings.serialization.decodeValue
17+
import com.russhwolf.settings.serialization.encodeValue
18+
import kotlinx.coroutines.CoroutineDispatcher
19+
import kotlinx.coroutines.flow.Flow
20+
import kotlinx.coroutines.flow.MutableStateFlow
21+
import kotlinx.coroutines.withContext
22+
import kotlinx.serialization.ExperimentalSerializationApi
23+
import kotlinx.serialization.builtins.ListSerializer
24+
import org.mifospay.core.model.autopay.Bill
25+
26+
private const val BILLS_KEY = "bills"
27+
28+
class BillDataSource(
29+
private val settings: Settings,
30+
private val dispatcher: CoroutineDispatcher,
31+
) {
32+
private val _bills = MutableStateFlow(
33+
settings.decodeValue(
34+
key = BILLS_KEY,
35+
serializer = ListSerializer(Bill.serializer()),
36+
defaultValue = emptyList(),
37+
),
38+
)
39+
40+
val bills: Flow<List<Bill>> = _bills
41+
42+
suspend fun updateBills(bills: List<Bill>) {
43+
withContext(dispatcher) {
44+
settings.putBills(bills)
45+
_bills.value = bills
46+
}
47+
}
48+
49+
suspend fun addBill(bill: Bill) {
50+
withContext(dispatcher) {
51+
val currentBills = _bills.value.toMutableList()
52+
if (!currentBills.any { it.id == bill.id }) {
53+
currentBills.add(bill)
54+
settings.putBills(currentBills)
55+
_bills.value = currentBills
56+
}
57+
}
58+
}
59+
60+
suspend fun removeBill(billId: String) {
61+
withContext(dispatcher) {
62+
val currentBills = _bills.value.toMutableList()
63+
currentBills.removeAll { it.id == billId }
64+
settings.putBills(currentBills)
65+
_bills.value = currentBills
66+
}
67+
}
68+
69+
suspend fun clearBills() {
70+
withContext(dispatcher) {
71+
settings.remove(BILLS_KEY)
72+
_bills.value = emptyList()
73+
}
74+
}
75+
}
76+
77+
private fun Settings.putBills(bills: List<Bill>) {
78+
encodeValue(
79+
key = BILLS_KEY,
80+
serializer = ListSerializer(Bill.serializer()),
81+
value = bills,
82+
)
83+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024 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 https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.datastore
11+
12+
import kotlinx.coroutines.flow.Flow
13+
import org.mifospay.core.common.DataState
14+
import org.mifospay.core.model.autopay.Bill
15+
16+
interface BillRepository {
17+
/**
18+
* Get all saved bills
19+
*/
20+
fun getAllBills(): Flow<List<Bill>>
21+
22+
/**
23+
* Get bill by ID
24+
*/
25+
suspend fun getBillById(id: String): Bill?
26+
27+
/**
28+
* Save a new bill
29+
*/
30+
suspend fun saveBill(bill: Bill): DataState<Bill>
31+
32+
/**
33+
* Update an existing bill
34+
*/
35+
suspend fun updateBill(bill: Bill): DataState<Bill>
36+
37+
/**
38+
* Delete a bill
39+
*/
40+
suspend fun deleteBill(id: String): DataState<Unit>
41+
42+
/**
43+
* Search bills by name
44+
*/
45+
suspend fun searchBillsByName(query: String): List<Bill>
46+
47+
/**
48+
* Clear all bills
49+
*/
50+
suspend fun clearAllBills(): DataState<Unit>
51+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2024 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 https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.datastore
11+
12+
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.first
14+
import org.mifospay.core.common.DataState
15+
import org.mifospay.core.model.autopay.Bill
16+
17+
/**
18+
* TODO: This implementation currently uses local storage (Multiplatform Settings) for bill data.
19+
* When the backend APIs for bill management are clarified and implemented, this should be
20+
* refactored to use network-based repository pattern similar to other repositories in the codebase
21+
* (e.g., UserRepositoryImpl, BeneficiaryRepositoryImpl) with proper API integration.
22+
*/
23+
class BillRepositoryImpl(
24+
private val billDataSource: BillDataSource,
25+
) : BillRepository {
26+
27+
override fun getAllBills(): Flow<List<Bill>> {
28+
return billDataSource.bills
29+
}
30+
31+
override suspend fun getBillById(id: String): Bill? {
32+
return try {
33+
val bills = billDataSource.bills.first()
34+
bills.find { it.id == id }
35+
} catch (e: Exception) {
36+
null
37+
}
38+
}
39+
40+
override suspend fun saveBill(bill: Bill): DataState<Bill> {
41+
return try {
42+
val existingBills = billDataSource.bills.first()
43+
44+
// Check if bill already exists
45+
val existingBill = existingBills.find {
46+
it.name == bill.name && it.billerId == bill.billerId
47+
}
48+
49+
if (existingBill != null) {
50+
DataState.Error(Exception("Bill with this name and biller already exists"))
51+
} else {
52+
billDataSource.addBill(bill)
53+
DataState.Success(bill)
54+
}
55+
} catch (e: Exception) {
56+
DataState.Error(Exception("Failed to save bill: ${e.message}"))
57+
}
58+
}
59+
60+
override suspend fun updateBill(bill: Bill): DataState<Bill> {
61+
return try {
62+
val existingBills = billDataSource.bills.first().toMutableList()
63+
val index = existingBills.indexOfFirst { it.id == bill.id }
64+
65+
if (index != -1) {
66+
existingBills[index] = bill
67+
billDataSource.updateBills(existingBills)
68+
DataState.Success(bill)
69+
} else {
70+
DataState.Error(Exception("Bill not found"))
71+
}
72+
} catch (e: Exception) {
73+
DataState.Error(Exception("Failed to update bill: ${e.message}"))
74+
}
75+
}
76+
77+
override suspend fun deleteBill(id: String): DataState<Unit> {
78+
return try {
79+
billDataSource.removeBill(id)
80+
DataState.Success(Unit)
81+
} catch (e: Exception) {
82+
DataState.Error(Exception("Failed to delete bill: ${e.message}"))
83+
}
84+
}
85+
86+
override suspend fun searchBillsByName(query: String): List<Bill> {
87+
return try {
88+
val bills = billDataSource.bills.first()
89+
bills.filter { it.name.contains(query, ignoreCase = true) }
90+
} catch (e: Exception) {
91+
emptyList()
92+
}
93+
}
94+
95+
override suspend fun clearAllBills(): DataState<Unit> {
96+
return try {
97+
billDataSource.clearBills()
98+
DataState.Success(Unit)
99+
} catch (e: Exception) {
100+
DataState.Error(Exception("Failed to clear bills: ${e.message}"))
101+
}
102+
}
103+
}

core/datastore/src/commonMain/kotlin/org/mifospay/core/datastore/di/PreferenceModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import org.mifospay.core.common.MifosDispatchers
1616
import org.mifospay.core.datastore.AutoPayPreferencesDataSource
1717
import org.mifospay.core.datastore.AutoPayPreferencesRepository
1818
import org.mifospay.core.datastore.AutoPayPreferencesRepositoryImpl
19+
import org.mifospay.core.datastore.BillDataSource
20+
import org.mifospay.core.datastore.BillRepository
21+
import org.mifospay.core.datastore.BillRepositoryImpl
1922
import org.mifospay.core.datastore.BillerDataSource
2023
import org.mifospay.core.datastore.BillerRepository
2124
import org.mifospay.core.datastore.BillerRepositoryImpl
@@ -29,6 +32,7 @@ val PreferencesModule = module {
2932
factory { UserPreferencesDataSource(get(), get(named(MifosDispatchers.IO.name))) }
3033
factory { AutoPayPreferencesDataSource(get(), get(named(MifosDispatchers.IO.name))) }
3134
factory { BillerDataSource(get(), get(named(MifosDispatchers.IO.name))) }
35+
factory { BillDataSource(get(), get(named(MifosDispatchers.IO.name))) }
3236

3337
single<UserPreferencesRepository> {
3438
UserPreferencesRepositoryImpl(
@@ -51,4 +55,10 @@ val PreferencesModule = module {
5155
billerDataSource = get(),
5256
)
5357
}
58+
59+
single<BillRepository> {
60+
BillRepositoryImpl(
61+
billDataSource = get(),
62+
)
63+
}
5464
}

0 commit comments

Comments
 (0)