Skip to content

Commit 3b1fb25

Browse files
committed
feat(feature:autopay): implement edit biller and biller list
1 parent 8f19f14 commit 3b1fb25

File tree

12 files changed

+1302
-47
lines changed

12 files changed

+1302
-47
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.mifospay.feature.autopay.navigateToAutoPayPreferences
3131
import org.mifospay.feature.autopay.navigateToAutoPayRules
3232
import org.mifospay.feature.autopay.navigateToAutoPayScheduleDetails
3333
import org.mifospay.feature.autopay.navigateToAutoPaySetup
34+
import org.mifospay.feature.autopay.navigateToBillerList
3435
import org.mifospay.feature.editpassword.navigation.editPasswordScreen
3536
import org.mifospay.feature.editpassword.navigation.navigateToEditPassword
3637
import org.mifospay.feature.faq.navigation.faqScreen
@@ -156,6 +157,9 @@ internal fun MifosNavHost(
156157
onNavigateToAddBiller = {
157158
navController.navigateToAddBiller()
158159
},
160+
onNavigateToBillerList = {
161+
navController.navigateToBillerList()
162+
},
159163
showTopBar = false,
160164
)
161165
},
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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 co.touchlab.kermit.Logger
13+
import org.mifospay.core.model.autopay.BillerFormData
14+
import org.mifospay.core.model.autopay.BillerValidationResult
15+
16+
/**
17+
* Validator for biller form data with comprehensive validation rules.
18+
*
19+
* Provides validation for all biller fields including:
20+
* - Name validation (required, length, format)
21+
* - Account number validation (required, format, length)
22+
* - Contact number validation (required, format, length)
23+
* - Email validation (optional, format)
24+
* - Category validation (required)
25+
* - Address validation (optional, length)
26+
*/
27+
object BillerValidator {
28+
29+
private val logger = Logger.withTag("BILLER_VALIDATOR")
30+
31+
// Validation constants
32+
private const val MIN_NAME_LENGTH = 2
33+
private const val MAX_NAME_LENGTH = 100
34+
private const val MIN_ACCOUNT_NUMBER_LENGTH = 8
35+
private const val MAX_ACCOUNT_NUMBER_LENGTH = 20
36+
private const val MIN_CONTACT_NUMBER_LENGTH = 10
37+
private const val MAX_CONTACT_NUMBER_LENGTH = 15
38+
private const val MAX_EMAIL_LENGTH = 254
39+
private const val MAX_ADDRESS_LENGTH = 500
40+
41+
// Regex patterns
42+
private val NAME_PATTERN = Regex("^[a-zA-Z0-9\\s\\-_.]+$")
43+
private val ACCOUNT_NUMBER_PATTERN = Regex("^[0-9]+$")
44+
private val CONTACT_NUMBER_PATTERN = Regex("^[+]?[0-9\\s\\-()]+$")
45+
private val EMAIL_PATTERN = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
46+
47+
/**
48+
* Validates the complete biller form data and returns validation result.
49+
*
50+
* @param formData The biller form data to validate
51+
* @return BillerValidationResult containing validation status and error messages
52+
*/
53+
fun validateBillerForm(formData: BillerFormData): BillerValidationResult {
54+
logger.d("BILLER_VALIDATOR Starting validation for biller: ${formData.name}")
55+
56+
val nameError = validateName(formData.name)
57+
val accountNumberError = validateAccountNumber(formData.accountNumber)
58+
val contactNumberError = validateContactNumber(formData.contactNumber)
59+
val emailError = validateEmail(formData.email)
60+
val categoryError = validateCategory(formData.category)
61+
val addressError = validateAddress(formData.address)
62+
63+
val isValid = nameError == null &&
64+
accountNumberError == null &&
65+
contactNumberError == null &&
66+
emailError == null &&
67+
categoryError == null &&
68+
addressError == null
69+
70+
val validationResult = BillerValidationResult(
71+
isValid = isValid,
72+
nameError = nameError,
73+
accountNumberError = accountNumberError,
74+
contactNumberError = contactNumberError,
75+
emailError = emailError,
76+
categoryError = categoryError,
77+
)
78+
79+
logger.d("BILLER_VALIDATOR Validation completed. Valid: $isValid")
80+
return validationResult
81+
}
82+
83+
/**
84+
* Validates biller name with length and format requirements.
85+
*
86+
* @param name The biller name to validate
87+
* @return Error message if invalid, null if valid
88+
*/
89+
private fun validateName(name: String): String? {
90+
return when {
91+
name.isBlank() -> "Biller name is required"
92+
name.length < MIN_NAME_LENGTH -> "Biller name must be at least $MIN_NAME_LENGTH characters"
93+
name.length > MAX_NAME_LENGTH -> "Biller name must be less than $MAX_NAME_LENGTH characters"
94+
!NAME_PATTERN.matches(name.trim()) -> "Biller name contains invalid characters"
95+
name.trim().isEmpty() -> "Biller name cannot be only whitespace"
96+
else -> null
97+
}
98+
}
99+
100+
/**
101+
* Validates account number with format and length requirements.
102+
*
103+
* @param accountNumber The account number to validate
104+
* @return Error message if invalid, null if valid
105+
*/
106+
private fun validateAccountNumber(accountNumber: String): String? {
107+
return when {
108+
accountNumber.isBlank() -> "Account number is required"
109+
accountNumber.length < MIN_ACCOUNT_NUMBER_LENGTH -> "Account number must be at least $MIN_ACCOUNT_NUMBER_LENGTH digits"
110+
accountNumber.length > MAX_ACCOUNT_NUMBER_LENGTH -> "Account number must be less than $MAX_ACCOUNT_NUMBER_LENGTH digits"
111+
!ACCOUNT_NUMBER_PATTERN.matches(accountNumber.trim()) -> "Account number must contain only digits"
112+
accountNumber.trim().isEmpty() -> "Account number cannot be only whitespace"
113+
else -> null
114+
}
115+
}
116+
117+
/**
118+
* Validates contact number with format and length requirements.
119+
*
120+
* @param contactNumber The contact number to validate
121+
* @return Error message if invalid, null if valid
122+
*/
123+
private fun validateContactNumber(contactNumber: String): String? {
124+
return when {
125+
contactNumber.isBlank() -> "Contact number is required"
126+
contactNumber.length < MIN_CONTACT_NUMBER_LENGTH -> "Contact number must be at least $MIN_CONTACT_NUMBER_LENGTH digits"
127+
contactNumber.length > MAX_CONTACT_NUMBER_LENGTH -> "Contact number must be less than $MAX_CONTACT_NUMBER_LENGTH digits"
128+
!CONTACT_NUMBER_PATTERN.matches(contactNumber.trim()) -> "Contact number contains invalid characters"
129+
contactNumber.trim().isEmpty() -> "Contact number cannot be only whitespace"
130+
else -> null
131+
}
132+
}
133+
134+
/**
135+
* Validates email address format (optional field).
136+
*
137+
* @param email The email address to validate
138+
* @return Error message if invalid, null if valid
139+
*/
140+
private fun validateEmail(email: String): String? {
141+
return when {
142+
email.isBlank() -> null // Email is optional
143+
email.length > MAX_EMAIL_LENGTH -> "Email address is too long (max $MAX_EMAIL_LENGTH characters)"
144+
!EMAIL_PATTERN.matches(email.trim()) -> "Please enter a valid email address"
145+
email.trim().isEmpty() -> "Email cannot be only whitespace"
146+
else -> null
147+
}
148+
}
149+
150+
/**
151+
* Validates that a category has been selected.
152+
*
153+
* @param category The selected category to validate
154+
* @return Error message if invalid, null if valid
155+
*/
156+
private fun validateCategory(category: org.mifospay.core.model.autopay.BillerCategory?): String? {
157+
return when {
158+
category == null -> "Please select a biller category"
159+
else -> null
160+
}
161+
}
162+
163+
/**
164+
* Validates address length (optional field).
165+
*
166+
* @param address The address to validate
167+
* @return Error message if invalid, null if valid
168+
*/
169+
private fun validateAddress(address: String): String? {
170+
return when {
171+
address.isBlank() -> null // Address is optional
172+
address.length > MAX_ADDRESS_LENGTH -> "Address is too long (max $MAX_ADDRESS_LENGTH characters)"
173+
address.trim().isEmpty() -> "Address cannot be only whitespace"
174+
else -> null
175+
}
176+
}
177+
178+
/**
179+
* Validates individual name field for real-time validation.
180+
*
181+
* @param name The name to validate
182+
* @return Error message if invalid, null if valid
183+
*/
184+
fun validateNameField(name: String): String? = validateName(name)
185+
186+
/**
187+
* Validates individual account number field for real-time validation.
188+
*
189+
* @param accountNumber The account number to validate
190+
* @return Error message if invalid, null if valid
191+
*/
192+
fun validateAccountNumberField(accountNumber: String): String? = validateAccountNumber(accountNumber)
193+
194+
/**
195+
* Validates individual contact number field for real-time validation.
196+
*
197+
* @param contactNumber The contact number to validate
198+
* @return Error message if invalid, null if valid
199+
*/
200+
fun validateContactNumberField(contactNumber: String): String? = validateContactNumber(contactNumber)
201+
202+
/**
203+
* Validates individual email field for real-time validation.
204+
*
205+
* @param email The email to validate
206+
* @return Error message if invalid, null if valid
207+
*/
208+
fun validateEmailField(email: String): String? = validateEmail(email)
209+
}

feature/autopay/src/commonMain/kotlin/org/mifospay/feature/autopay/AddBillerScreen.kt

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package org.mifospay.feature.autopay
1111

1212
import androidx.compose.foundation.layout.Arrangement
1313
import androidx.compose.foundation.layout.Column
14+
import androidx.compose.foundation.layout.Row
1415
import androidx.compose.foundation.layout.Spacer
1516
import androidx.compose.foundation.layout.fillMaxSize
1617
import androidx.compose.foundation.layout.fillMaxWidth
@@ -37,6 +38,7 @@ import org.mifospay.core.designsystem.component.LoadingDialogState
3738
import org.mifospay.core.designsystem.component.MifosBasicDialog
3839
import org.mifospay.core.designsystem.component.MifosButton
3940
import org.mifospay.core.designsystem.component.MifosLoadingDialog
41+
import org.mifospay.core.designsystem.component.MifosOutlinedButton
4042
import org.mifospay.core.designsystem.component.MifosOutlinedTextField
4143
import org.mifospay.core.designsystem.component.MifosScaffold
4244
import org.mifospay.core.designsystem.component.MifosTopBar
@@ -50,6 +52,7 @@ import org.mifospay.core.ui.utils.EventsEffect
5052
@Composable
5153
fun AddBillerScreen(
5254
onNavigateBack: () -> Unit,
55+
onNavigateToBillerList: () -> Unit,
5356
viewModel: AddBillerViewModel = koinViewModel(),
5457
) {
5558
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@@ -58,7 +61,7 @@ fun AddBillerScreen(
5861
EventsEffect(viewModel) { event ->
5962
when (event) {
6063
is AddBillerEvent.BillerSaved -> {
61-
onNavigateBack()
64+
onNavigateToBillerList()
6265
}
6366
}
6467
}
@@ -166,12 +169,22 @@ fun AddBillerScreen(
166169

167170
Spacer(modifier = Modifier.height(24.dp))
168171

169-
MifosButton(
170-
text = { Text("Save Biller") },
171-
onClick = { viewModel.trySendAction(AddBillerAction.SaveBiller) },
172+
Row(
172173
modifier = Modifier.fillMaxWidth(),
173-
enabled = !state.isLoading,
174-
)
174+
horizontalArrangement = Arrangement.spacedBy(16.dp),
175+
) {
176+
MifosOutlinedButton(
177+
text = { Text("Cancel") },
178+
onClick = onNavigateBack,
179+
modifier = Modifier.weight(1f),
180+
)
181+
MifosButton(
182+
text = { Text("Save Biller") },
183+
onClick = { viewModel.trySendAction(AddBillerAction.SaveBiller) },
184+
modifier = Modifier.weight(1f),
185+
enabled = !state.isLoading,
186+
)
187+
}
175188
}
176189
}
177190

@@ -187,7 +200,7 @@ fun AddBillerScreen(
187200
title = "Error",
188201
message = error,
189202
),
190-
onDismissRequest = { viewModel.trySendAction(AddBillerAction.ValidateForm) },
203+
onDismissRequest = { viewModel.trySendAction(AddBillerAction.ClearError) },
191204
)
192205
}
193206
}

0 commit comments

Comments
 (0)