From 6a29241c74649fc39c7827de0a4a3149e6b597dd Mon Sep 17 00:00:00 2001 From: Varun Jain Date: Fri, 4 Jul 2025 19:14:39 +0530 Subject: [PATCH] NTP-71871 | Fund details SIP simulator (#16837) --- .../widgets/ReturnsCalculatorComposable.kt | 474 ++++++++++++++++++ .../fundbuy/fragments/FundDetailsFragment.kt | 10 + .../fundbuy/models/FundDetailScreenData.kt | 37 ++ .../models/cards/InvestmentGoalCardData.kt | 2 + .../main/java/com/navi/amc/utils/AmcColor.kt | 1 + .../java/com/navi/amc/utils/CommonUtils.kt | 16 + .../main/java/com/navi/amc/utils/Constant.kt | 2 + 7 files changed, 542 insertions(+) create mode 100644 android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/ReturnsCalculatorComposable.kt diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/ReturnsCalculatorComposable.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/ReturnsCalculatorComposable.kt new file mode 100644 index 0000000000..e020eecf85 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/composables/widgets/ReturnsCalculatorComposable.kt @@ -0,0 +1,474 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.composables.widgets + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.amc.fundbuy.models.ReturnsCalculatorData +import com.navi.amc.utils.AmcColor +import com.navi.amc.utils.AmcColor.rippleColor +import com.navi.amc.utils.Constant.DEFAULT +import com.navi.amc.utils.Constant.SELECTED +import com.navi.amc.utils.calculateReturns +import com.navi.amc.utils.convertToTextFieldData +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.design.textview.model.NaviSpan +import com.navi.design.textview.model.TextWithStyle +import com.navi.design.utils.NoRippleIndicationSource +import com.navi.design.utils.decimalFormatForAmount +import com.navi.naviwidgets.composewidget.reusable.colorCTAPrimary +import com.navi.naviwidgets.composewidget.reusable.colorOffWhite +import com.navi.naviwidgets.composewidget.reusable.colorShadow +import com.navi.naviwidgets.composewidget.reusable.colorTextPrimary +import com.navi.naviwidgets.extensions.NaviImage +import com.navi.naviwidgets.extensions.NaviText +import com.navi.naviwidgets.extensions.NaviTextWidgetized +import com.navi.naviwidgets.models.response.ImageFieldData +import com.navi.uitron.utils.hexToComposeColor +import kotlin.math.roundToLong + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReturnsCalculatorComposable(calculatorData: ReturnsCalculatorData? = null) { + calculatorData?.let { data -> + var isExpanded by remember { mutableStateOf(data.isExpanded == true) } + var selectedDuration by remember { mutableStateOf(data.duration?.defaultSelected) } + var sliderValue by remember { mutableLongStateOf(data.sliderData?.selectedValue ?: 20000) } + + val (cagr, durationInMonths, returnsPercentage) = + remember(selectedDuration) { + val item = data.duration?.items?.find { it.key == selectedDuration } + Triple(item?.cagrValue ?: 0.0, item?.durationInMonths ?: 1, item?.returns) + } + + val totalInvestment = + remember(sliderValue, durationInMonths) { sliderValue.toLong() * durationInMonths } + + val projectedAmount = + remember(sliderValue, cagr, durationInMonths) { + calculateReturns( + monthlyAmount = sliderValue.toDouble(), + cagrInPercentage = cagr, + durationInMonths = durationInMonths, + ) + } + + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Row( + modifier = + Modifier.fillMaxWidth().clickable( + indication = null, + interactionSource = remember { NoRippleIndicationSource() }, + ) { + isExpanded = !isExpanded + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + data.title?.let { title -> + NaviTextWidgetized(textFieldData = convertToTextFieldData(title)) + } + + val rotationState by + animateFloatAsState( + targetValue = if (isExpanded) 180f else 0f, + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + ) + + NaviImage( + imageFieldData = ImageFieldData(url = data.rightChevronDown), + modifier = Modifier.size(24.dp).rotate(rotationState), + ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = + expandVertically( + animationSpec = tween(durationMillis = 400), + expandFrom = Alignment.Top, + ), + exit = + shrinkVertically( + animationSpec = tween(durationMillis = 400), + shrinkTowards = Alignment.Top, + ), + ) { + Column { + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + data.duration?.title?.let { title -> + NaviTextWidgetized(textFieldData = convertToTextFieldData(title)) + } + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + data.duration?.items?.forEach { item -> + val isSelected = item.key == selectedDuration + val spanList = + if (isSelected) { + item.title?.styleVariations?.get(SELECTED) + } else { + item.title?.styleVariations?.get(DEFAULT) + } + val span = spanList?.firstOrNull() + DurationChip( + text = item.title?.text.orEmpty(), + isSelected = isSelected, + styleSpan = span, + onClick = { selectedDuration = item.key ?: "1Y" }, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + SliderSection( + data = data, + sliderValue = sliderValue, + onValueChange = { newValue -> sliderValue = newValue }, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + NoteSection( + data = data, + totalInvestment = totalInvestment, + projectedAmount = projectedAmount, + returns = returnsPercentage, + ) + + Spacer(modifier = Modifier.height(4.dp)) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SliderSection( + data: ReturnsCalculatorData, + sliderValue: Long, + onValueChange: (Long) -> Unit = {}, +) { + val layoutWidthPx = remember { mutableStateOf(null) } + val minSliderValue = data.sliderData?.minValue?.toFloat() ?: 100f + val maxSliderValue = data.sliderData?.maxValue?.toFloat() ?: 100000f + val sliderStepValue = data.sliderData?.stepValue?.toInt() ?: 100 + val interactionSource = remember { NoRippleIndicationSource() } + + val thumbPosition = + ((sliderValue.toFloat() - minSliderValue) / (maxSliderValue - minSliderValue)).coerceIn( + 0f, + 1f, + ) + + Box( + modifier = + Modifier.fillMaxWidth().onGloballyPositioned { coordinates -> + layoutWidthPx.value = coordinates.size.width.toFloat() + } + ) { + Slider( + value = sliderValue.toFloat(), + onValueChange = { + val normalizedValue = (it - minSliderValue) / (maxSliderValue - minSliderValue) + val customValue = + when { + normalizedValue < 0.01f -> minSliderValue.toLong() + else -> { + val stepIndex = ((normalizedValue - 0.01f) / 0.99f * 99).roundToLong() + 1000L + stepIndex * 1000L + } + } + onValueChange(customValue) + }, + valueRange = minSliderValue..maxSliderValue, + steps = ((maxSliderValue - minSliderValue) / sliderStepValue).toInt() - 1, + modifier = Modifier.fillMaxWidth().padding(top = 20.dp), + colors = + SliderDefaults.colors( + thumbColor = AmcColor.purplePrimary, + activeTrackColor = + data.sliderData?.minTrackTintColor?.hexToComposeColor + ?: AmcColor.purplePrimary, + inactiveTrackColor = + data.sliderData?.maxTrackTintColor?.hexToComposeColor + ?: AmcColor.borderDefault, + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ), + thumb = { + Box( + modifier = + Modifier.shadow( + elevation = 4.dp, + shape = CircleShape, + spotColor = colorTextPrimary, + ambientColor = colorTextPrimary, + ) + .size(24.dp) + .background(Color.White, shape = CircleShape), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier.size(20.dp) + .background(AmcColor.purplePrimary, shape = CircleShape) + ) + } + }, + track = { sliderPositions -> + val fraction by remember { + derivedStateOf { + (sliderPositions.value - sliderPositions.valueRange.start) / + (sliderPositions.valueRange.endInclusive - + sliderPositions.valueRange.start) + } + } + Box( + modifier = + Modifier.shadow( + elevation = 4.dp, + spotColor = colorShadow, + ambientColor = colorShadow, + ) + .fillMaxWidth() + .height(4.dp) + .background( + data.sliderData?.maxTrackTintColor?.hexToComposeColor + ?: AmcColor.borderDefault, + shape = RoundedCornerShape(2.dp), + ) + ) + Box( + modifier = + Modifier.shadow( + elevation = 4.dp, + spotColor = colorShadow, + ambientColor = colorShadow, + ) + .fillMaxWidth(fraction) + .height(4.dp) + .background( + data.sliderData?.minTrackTintColor?.hexToComposeColor + ?: AmcColor.purplePrimary, + shape = RoundedCornerShape(2.dp), + ) + ) + }, + interactionSource = interactionSource, + ) + + if (layoutWidthPx.value != null) { + val density = LocalDensity.current + val layoutWidth = layoutWidthPx.value!! + + val thumbRadiusPx = with(density) { 12.dp.toPx() } + val toolTipData = convertToTextFieldData(data.sliderData?.toolTipData) + val formattedSliderValue = decimalFormatForAmount.format(sliderValue) + toolTipData?.text = "₹${formattedSliderValue}/month" + + val tooltipWidth = + when { + sliderValue >= 100000 -> 140.dp + sliderValue >= 1000 -> 130.dp + else -> 110.dp + } + + val tooltipWidthPx = with(density) { tooltipWidth.toPx() } + val adjustedTrackWidth = layoutWidth - 2 * thumbRadiusPx + val thumbCenterX = thumbRadiusPx + thumbPosition * adjustedTrackWidth + + val tooltipHalfWidth = tooltipWidthPx / 2 + val tooltipCenterX = + thumbCenterX.coerceIn(tooltipHalfWidth, layoutWidth - tooltipHalfWidth) + val tooltipLeft = tooltipCenterX - tooltipHalfWidth + + val triangleWidthPx = with(density) { 10.dp.toPx() } + val triangleOffsetX = + (thumbCenterX - tooltipLeft - triangleWidthPx / 2).coerceIn( + 0f, + tooltipWidthPx - triangleWidthPx, + ) + + Box( + modifier = + Modifier.offset(x = with(density) { tooltipLeft.toDp() }, y = (-4).dp) + .width(with(density) { tooltipWidthPx.toDp() }) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = + Modifier.align(Alignment.CenterHorizontally) + .background(colorCTAPrimary, shape = RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 6.dp) + .fillMaxWidth() + ) { + NaviTextWidgetized(toolTipData, modifier = Modifier.align(Alignment.Center)) + } + + Box(modifier = Modifier.fillMaxWidth()) { + Canvas( + modifier = + Modifier.size(10.dp, 5.dp) + .offset(x = with(density) { triangleOffsetX.toDp() }) + ) { + val path = + Path().apply { + moveTo(0f, 0f) + lineTo(size.width, 0f) + lineTo(size.width / 2f, size.height) + close() + } + drawPath(path, color = colorCTAPrimary) + } + } + } + } + } + } +} + +@Composable +fun NoteSection( + data: ReturnsCalculatorData? = null, + totalInvestment: Long = 0, + projectedAmount: Double = 0.0, + returns: TextWithStyle? = null, +) { + val formattedInvestment by + remember(totalInvestment) { mutableStateOf(decimalFormatForAmount.format(totalInvestment)) } + + val formattedProjectedAmount by + remember(projectedAmount) { + mutableStateOf(decimalFormatForAmount.format(projectedAmount.roundToLong())) + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = colorOffWhite), + shape = RoundedCornerShape(4.dp), + elevation = CardDefaults.cardElevation(0.dp), + ) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { + val noteData = data?.investmentNote + Row(verticalAlignment = Alignment.CenterVertically) { + NaviTextWidgetized(textFieldData = convertToTextFieldData(noteData?.startText)) + val investedAmountData = + convertToTextFieldData(noteData?.investedAmount)?.apply { + text = "₹$formattedInvestment" + } + NaviTextWidgetized(textFieldData = investedAmountData) + Spacer(modifier = Modifier.width(2.dp)) + NaviTextWidgetized(textFieldData = convertToTextFieldData(noteData?.endText)) + } + + Spacer(modifier = Modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + val projectedAmountData = + convertToTextFieldData(noteData?.projectedAmount)?.apply { + text = "₹$formattedProjectedAmount" + } + NaviTextWidgetized(textFieldData = projectedAmountData) + Spacer(modifier = Modifier.width(3.dp)) + NaviTextWidgetized(textFieldData = convertToTextFieldData(returns)) + } + } + } +} + +@Composable +fun DurationChip( + text: String, + styleSpan: NaviSpan? = null, + isSelected: Boolean, + onClick: () -> Unit, +) { + Box( + modifier = + Modifier.clip(RoundedCornerShape(20.dp)) + .border( + width = 1.dp, + color = if (isSelected) colorCTAPrimary else Color.Transparent, + shape = RoundedCornerShape(20.dp), + ) + .background(Color.Transparent) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(color = rippleColor), + ) { + onClick() + } + .padding(horizontal = 12.dp, vertical = 4.dp), + contentAlignment = Alignment.Center, + ) { + NaviText( + text = text, + fontFamily = naviFontFamily, + color = + styleSpan?.spanColor?.hexToComposeColor + ?: if (isSelected) colorCTAPrimary else Color.Transparent, + fontSize = (styleSpan?.fontSize?.toFloat() ?: 12f).sp, + fontWeight = getFontWeight(styleSpan?.fontName), + ) + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt index 76c97a813d..ce604fa06a 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt @@ -14,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.view.ViewStub import android.widget.ImageView +import androidx.compose.ui.platform.ComposeView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams @@ -35,6 +36,7 @@ import com.navi.amc.compose.feature.ftue.model.FundSelectionData import com.navi.amc.databinding.DownloadDocumentLayoutBinding import com.navi.amc.databinding.FundDetailScreenLayoutBinding import com.navi.amc.databinding.InvestUspLayoutBinding +import com.navi.amc.fundbuy.composables.widgets.ReturnsCalculatorComposable import com.navi.amc.fundbuy.models.AmcHeaderData import com.navi.amc.fundbuy.models.FundReturn import com.navi.amc.fundbuy.viewmodel.FundBuyFlowViewModel @@ -367,6 +369,14 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { } } + fundDetailScreenData?.content?.returnsCalculatorDetails?.let { calculatorData -> + val calculatorView = + ComposeView(context).apply { + setContent { ReturnsCalculatorComposable(calculatorData) } + } + addView(calculatorView) + } + fundDetailScreenData?.content?.usp?.let { val childBinding = DataBindingUtil.inflate( diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt index 2711010ef4..823f370751 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt @@ -12,9 +12,11 @@ import com.google.gson.annotations.SerializedName import com.navi.amc.common.model.Footer import com.navi.amc.common.model.FundInvestmentDetailData import com.navi.amc.common.model.InformationCardData +import com.navi.amc.fundbuy.models.cards.SliderData import com.navi.common.model.Header import com.navi.design.textview.model.NaviSpan import com.navi.design.textview.model.TextWithStyle +import com.navi.design.textview.model.TextWithStyleVariation import com.navi.naviwidgets.models.FundDetailData import com.navi.naviwidgets.models.response.amc.TagData import java.io.Serializable @@ -44,6 +46,41 @@ data class FundDetailScreenData( @SerializedName("fundInvestmentDetails") val fundInvestmentDetails: FundInvestmentDetailData? = null, @SerializedName("regulatoryInfo") val regulatoryInfo: RegulatoryInfoData? = null, + @SerializedName("returnsCalculatorDetails") + val returnsCalculatorDetails: ReturnsCalculatorData? = null, +) + +data class ReturnsCalculatorData( + @SerializedName("title") val title: TextWithStyle? = null, + @SerializedName("isExpanded") val isExpanded: Boolean? = false, + @SerializedName("rightChevronDown") val rightChevronDown: String? = null, + @SerializedName("rightChevronUp") val rightChevronUp: String? = null, + @SerializedName("sliderData") val sliderData: SliderData? = null, + @SerializedName("duration") val duration: DurationChipData? = null, + @SerializedName("investmentNote") val investmentNote: NoteData? = null, +) + +data class NoteData( + @SerializedName("startText") val startText: TextWithStyle? = null, + @SerializedName("endText") val endText: TextWithStyle? = null, + @SerializedName("investedAmount") val investedAmount: TextWithStyle? = null, + @SerializedName("projectedAmount") val projectedAmount: TextWithStyle? = null, + @SerializedName("returns") val returns: TextWithStyle? = null, +) + +data class DurationChipData( + @SerializedName("title") val title: TextWithStyle? = null, + @SerializedName("defaultSelected") val defaultSelected: String? = null, + @SerializedName("items") val items: List? = null, +) + +data class ChipItemData( + @SerializedName("title") val title: TextWithStyleVariation? = null, + @SerializedName("key") val key: String? = null, + @SerializedName("isSelected") val isSelected: Boolean? = null, + @SerializedName("cagrValue") val cagrValue: Double? = null, + @SerializedName("durationInMonths") val durationInMonths: Int? = null, + @SerializedName("returns") val returns: TextWithStyle? = null, ) @Parcelize diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt index 224d5f304d..e934553623 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/cards/InvestmentGoalCardData.kt @@ -11,6 +11,7 @@ import com.google.gson.annotations.SerializedName import com.navi.amc.fundbuy.models.widgets.TrackerContent import com.navi.base.model.ActionData import com.navi.common.model.InvestmentBaseProperty +import com.navi.design.textview.model.TextWithStyle import com.navi.naviwidgets.models.response.ImageFieldData import com.navi.naviwidgets.models.response.TextFieldData @@ -70,4 +71,5 @@ data class SliderData( @SerializedName("minTrackTintColor") val minTrackTintColor: String? = null, @SerializedName("maxTrackTintColor") val maxTrackTintColor: String? = null, @SerializedName("title") val title: TextFieldData? = null, + @SerializedName("toolTipData") val toolTipData: TextWithStyle? = null, ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/AmcColor.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/AmcColor.kt index c78a85f52d..dc9b235168 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/AmcColor.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/AmcColor.kt @@ -21,4 +21,5 @@ object AmcColor { val textInputNonEditable = Color(0xFF6B6B6B) val textInputPrimary = Color(0xFF191919) val purplePrimary = Color(0xFF3C0050) + val rippleColor = Color.Black.copy(alpha = 0.3f) } diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt index 1c2bada400..e633015558 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt @@ -55,6 +55,7 @@ import java.text.SimpleDateFormat import java.util.TimeZone import kotlin.math.floor import kotlin.math.max +import kotlin.math.pow fun calculateInvestmentReturnsAmount( investmentType: String, @@ -237,3 +238,18 @@ fun shouldCallUpdateSip(url: String?): Boolean { url?.contains(RESUMED_INSTALLMENT).orFalse() || url?.contains(GOAL_SIP_DELETED).orFalse() } + +fun calculateReturns( + monthlyAmount: Double, + cagrInPercentage: Double, + durationInMonths: Int, +): Double { + if (cagrInPercentage == 0.0) { + return monthlyAmount * durationInMonths + } + val annualRate = cagrInPercentage / 100 + val monthlyRateOfInterest = (1 + annualRate).pow(1.0 / 12) - 1 + return monthlyAmount * + ((((1 + monthlyRateOfInterest).pow(durationInMonths)) - 1) / monthlyRateOfInterest) * + (1 + monthlyRateOfInterest) +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt index 3b439f36cd..428e12cc9d 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/Constant.kt @@ -265,4 +265,6 @@ object Constant { const val VALUE = "value" const val SIP_TYPE_CAMEL_CASE = "sipType" const val SHOW_TOAST_MESSAGE = "showToastMessage" + const val SELECTED = "selected" + const val DEFAULT = "default" }