From 3ae6b6246797564643255b32d568eb2de38d43a5 Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 18 Jul 2024 13:34:35 +0200 Subject: [PATCH] feat(bank-sdk): Skonto screen. Backend Integration PP-480 --- .../bank/sdk/capture/CaptureFlowFragment.kt | 39 ++++++--- .../sdk/capture/skonto/SkontoDataExtractor.kt | 72 ++++++++++++++++ .../sdk/capture/skonto/SkontoException.kt | 57 ------------- .../bank/sdk/capture/skonto/SkontoFragment.kt | 64 ++++++++------ .../capture/skonto/SkontoFragmentContract.kt | 14 ++- .../capture/skonto/SkontoFragmentViewModel.kt | 72 ++++++++-------- .../sdk/capture/skonto/SkontoValidator.kt | 85 ------------------- .../sdk/capture/skonto/model/SkontoData.kt | 51 +++++++++++ .../src/main/res/navigation/gbs_nav_graph.xml | 7 +- 9 files changed, 235 insertions(+), 226 deletions(-) create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoDataExtractor.kt delete mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoException.kt delete mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoValidator.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoData.kt diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt index fef9df911..5faaef5d4 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt @@ -15,7 +15,7 @@ import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceException import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceFragment import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceFragmentListener import net.gini.android.bank.sdk.capture.digitalinvoice.LineItemsValidator -import net.gini.android.bank.sdk.capture.skonto.SkontoValidator +import net.gini.android.bank.sdk.capture.skonto.SkontoDataExtractor import net.gini.android.bank.sdk.util.disallowScreenshots import net.gini.android.capture.CaptureSDKResult import net.gini.android.capture.Document @@ -27,13 +27,14 @@ import net.gini.android.capture.camera.CameraFragmentListener import net.gini.android.capture.network.model.GiniCaptureCompoundExtraction import net.gini.android.capture.network.model.GiniCaptureSpecificExtraction import net.gini.android.capture.internal.util.CancelListener +import java.math.BigDecimal +import java.time.LocalDate class CaptureFlowFragment(private val openWithDocument: Document? = null) : Fragment(), GiniCaptureFragmentListener, DigitalInvoiceFragmentListener, - CancelListener -{ + CancelListener { private lateinit var navController: NavController private lateinit var captureFlowFragmentListener: CaptureFlowFragmentListener @@ -50,7 +51,8 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { val inflater = super.onGetLayoutInflater(savedInstanceState) - val contextThemeWrapper = ContextThemeWrapper(requireContext(), net.gini.android.capture.R.style.GiniCaptureTheme) + val contextThemeWrapper = + ContextThemeWrapper(requireContext(), net.gini.android.capture.R.style.GiniCaptureTheme) return inflater.cloneInContext(contextThemeWrapper) } @@ -71,7 +73,8 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : override fun onCreate(savedInstanceState: Bundle?) { - childFragmentManager.fragmentFactory = CaptureFlowFragmentFactory(this, openWithDocument, this, this) + childFragmentManager.fragmentFactory = + CaptureFlowFragmentFactory(this, openWithDocument, this, this) super.onCreate(savedInstanceState) if (GiniCapture.hasInstance() && !GiniCapture.getInstance().allowScreenshots) { requireActivity().window.disallowScreenshots() @@ -128,6 +131,7 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : finishWithResult(result) } } + else -> { didFinishWithResult = true captureFlowFragmentListener.onFinishedWithResult(result.toCaptureResult()) @@ -138,13 +142,20 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : private fun tryShowingSkontoScreen(result: CaptureSDKResult.Success) { if (GiniBank.getCaptureConfiguration()?.skontoEnabled == true) { try { - SkontoValidator.validate(result.compoundExtractions) - navController.navigate(GiniCaptureFragmentDirections.toSkontoFragment()) + val skontoData = SkontoDataExtractor.extractSkontoData( + result.specificExtractions, + result.compoundExtractions + ) + + navController.navigate( + GiniCaptureFragmentDirections.toSkontoFragment(data = skontoData) + ) } catch (e: Exception) { finishWithResult(result) } } } + private fun tryShowingReturnAssistant(result: CaptureSDKResult.Success) { LineItemsValidator.validate(result.compoundExtractions) navController.navigate( @@ -192,11 +203,13 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : compoundExtractions: Map ) { didFinishWithResult = true - captureFlowFragmentListener.onFinishedWithResult(CaptureResult.Success( - specificExtractions, - compoundExtractions, - emptyList() - )) + captureFlowFragmentListener.onFinishedWithResult( + CaptureResult.Success( + specificExtractions, + compoundExtractions, + emptyList() + ) + ) } override fun onCancelFlow() { @@ -239,10 +252,12 @@ class CaptureFlowFragmentFactory( giniCaptureFragmentListener ) } + DigitalInvoiceFragment::class.java.name -> DigitalInvoiceFragment().apply { listener = digitalInvoiceListener cancelListener = cancelCallback } + else -> super.instantiate(classLoader, className) } } diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoDataExtractor.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoDataExtractor.kt new file mode 100644 index 000000000..caa6fdf2c --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoDataExtractor.kt @@ -0,0 +1,72 @@ +package net.gini.android.bank.sdk.capture.skonto + +import net.gini.android.bank.sdk.capture.skonto.model.SkontoData +import net.gini.android.bank.sdk.capture.skonto.model.SkontoData.Amount +import net.gini.android.bank.sdk.capture.skonto.model.SkontoData.SkontoPaymentMethod +import net.gini.android.capture.network.model.GiniCaptureCompoundExtraction +import net.gini.android.capture.network.model.GiniCaptureExtraction +import net.gini.android.capture.network.model.GiniCaptureSpecificExtraction +import java.math.BigDecimal +import java.time.LocalDate + + +internal class SkontoDataExtractor { + + companion object { + + fun extractSkontoData( + extractions: Map, + compoundExtractions: Map, + ): SkontoData { + val totalAmountToPay = extractions["amountToPay"] + ?: throw NoSuchElementException("Field `extractions.amountToPay` is missing") + + val skontoDiscountMaps = compoundExtractions["skontoDiscounts"]?.specificExtractionMaps + ?: throw NoSuchElementException("Field `compoundExtractions.skontoDiscounts` is missing") + + return skontoDiscountMaps.map { skontoDiscountData -> + val skontoPercentageDiscounted = skontoDiscountData.extractDataByKeys( + "skontoPercentageDiscounted", + "skontoPercentageDiscountedCalculated", + ) ?: throw NoSuchElementException("Data for `PercentageDiscounted` is missing") + + val skontoPaymentMethod = + skontoDiscountData.extractDataByKeys("skontoPaymentMethod") + + + val skontoAmountToPay = skontoDiscountData.extractDataByKeys( + "skontoAmountToPay", + "skontoAmountToPayCalculated" + ) ?: throw NoSuchElementException("Skonto data for `AmountToPay` is missing") + + val skontoRemainingDays = skontoDiscountData.extractDataByKeys( + "skontoRemainingDays", + "skontoRemainingDaysCalculated" + ) ?: throw NoSuchElementException("Skonto data for `RemainingDays` is missing") + + val skontoDueDate = skontoDiscountData.extractDataByKeys( + "skontoDueDate", + "skontoDueDateCalculated" + ) ?: throw NoSuchElementException("Skonto data for `DueDate` is missing") + + val skontoAmountDiscounted = skontoDiscountData.extractDataByKeys( + "skontoAmountDiscounted", + "skontoAmountDiscountedCalculated" + ) + + SkontoData( + skontoPercentageDiscounted = BigDecimal(skontoPercentageDiscounted.value), + skontoAmountToPay = Amount.parse(skontoAmountToPay.value), + fullAmountToPay = Amount.parse(totalAmountToPay.value), + skontoRemainingDays = skontoRemainingDays.value.toInt(), + skontoDueDate = skontoDueDate.value.let(LocalDate::parse), + skontoPaymentMethod = skontoPaymentMethod?.let { SkontoPaymentMethod.valueOf(it.value) } + ) + }.first() + } + } +} + +fun Map.extractDataByKeys(vararg keys: String) = + keys.firstNotNullOfOrNull { this[it] } + diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoException.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoException.kt deleted file mode 100644 index 44be5843c..000000000 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoException.kt +++ /dev/null @@ -1,57 +0,0 @@ -package net.gini.android.bank.sdk.capture.skonto - -/** - * Exceptions related to the Skonto feature. - */ -sealed class SkontoException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { - - /** - * Internal use only. - * - * @suppress - */ - class SkontoMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) - - /** - * Internal use only. - * - * @suppress - */ - class DueDateMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) - - /** - * Internal use only. - * - * @suppress - */ - class AmountDiscountedMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) - - /** - * Internal use only. - * - * @suppress - */ - class AmountToPayMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) - - /** - * Internal use only. - * - * @suppress - */ - class RemainingDaysMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) - - /** - * Internal use only. - * - * @suppress - */ - class PaymentMethodMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) - - - /** - * Internal use only. - * - * @suppress - */ - class PercentageDiscountedMissingException(message: String? = null, cause: Throwable? = null) : SkontoException(message, cause) -} \ No newline at end of file diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt index bf5a7fefe..a9fae9f03 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt @@ -56,7 +56,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.navArgs import net.gini.android.bank.sdk.GiniBank import net.gini.android.bank.sdk.R import net.gini.android.bank.sdk.capture.skonto.colors.SkontoScreenColors @@ -64,7 +66,9 @@ import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoFooterSecti import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoiceScanSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.WithoutSkontoSectionColors +import net.gini.android.bank.sdk.capture.skonto.model.SkontoData import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol +import net.gini.android.capture.Amount import net.gini.android.capture.GiniCapture import net.gini.android.capture.ui.components.button.filled.GiniButton import net.gini.android.capture.ui.components.picker.date.GiniDatePickerDialog @@ -82,6 +86,8 @@ import java.time.format.DateTimeFormatter class SkontoFragment : Fragment() { + val args: SkontoFragmentArgs by navArgs() + private val isBottomNavigationBarEnabled = GiniCapture.getInstance().isBottomNavigationBarEnabled @@ -96,7 +102,10 @@ class SkontoFragment : Fragment() { customBottomNavigationBarView = container?.let { customBottomNavBarAdapter?.viewAdapter?.onCreateView(it) } - val viewModel = ViewModelProvider(requireActivity())[SkontoFragmentViewModel::class.java] + val viewModel = ViewModelProvider( + factory = ViewModelFactory(args.data), + owner = requireActivity() + )[SkontoFragmentViewModel::class.java] return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) @@ -111,6 +120,16 @@ class SkontoFragment : Fragment() { } } } + + internal class ViewModelFactory( + private val args: SkontoData, + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return SkontoFragmentViewModel(args) as T + } + } } @Composable @@ -154,7 +173,6 @@ private fun ScreenStateContent( screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors() ) { when (state) { - SkontoFragmentContract.State.Idle -> TODO() is SkontoFragmentContract.State.Ready -> ScreenReadyState( modifier = modifier, state = state, @@ -203,9 +221,8 @@ private fun ScreenReadyState( bottomBar = { FooterSection( colors = screenColorScheme.footerSectionColors, - discountValue = state.discountValue, + discountValue = state.discountAmount, totalAmount = state.totalAmount, - currency = state.currency, isBottomNavigationBarEnabled = isBottomNavigationBarEnabled, onBackClicked = onBackClicked, onHelpClicked = onHelpClicked, @@ -229,10 +246,9 @@ private fun ScreenReadyState( amount = state.skontoAmount, dueDate = state.discountDueDate, infoPaymentInDays = state.paymentInDays, - infoDiscountValue = state.discountValue, + infoDiscountValue = state.discountAmount, onActiveChange = onDiscountSectionActiveChange, isActive = state.isSkontoSectionActive, - currencyCode = "EUR", onSkontoAmountChange = onDiscountAmountChange, onDueDateChanged = onDueDateChanged, ) @@ -240,7 +256,6 @@ private fun ScreenReadyState( colors = screenColorScheme.withoutSkontoSectionColors, isActive = !state.isSkontoSectionActive, amount = state.fullAmount, - currencyCode = "EUR", onFullAmountChange = onFullAmountChange, ) } @@ -364,8 +379,7 @@ private fun YourInvoiceScanSection( @Composable private fun SkontoSection( colors: SkontoSectionColors, - amount: BigDecimal, - currencyCode: String, + amount: SkontoData.Amount, dueDate: LocalDate, infoPaymentInDays: Int, infoDiscountValue: BigDecimal, @@ -420,7 +434,7 @@ private fun SkontoSection( colors = colors.infoBannerColors, ) GiniAmountTextInput( - amount = amount, + amount = amount.amount, modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), @@ -431,7 +445,7 @@ private fun SkontoSection( trailingContent = { AnimatedVisibility(visible = isActive) { Text( - text = currencyCode, + text = amount.currencyCode, style = GiniTheme.typography.subtitle1, ) } @@ -476,7 +490,9 @@ private fun SkontoSection( onSaved = { isDatePickerVisible = false onDueDateChanged(it) - }) + }, + date = dueDate + ) } } @@ -518,8 +534,7 @@ private fun InfoBanner( @Composable private fun WithoutSkontoSection( colors: WithoutSkontoSectionColors, - amount: BigDecimal, - currencyCode: String, + amount: SkontoData.Amount, modifier: Modifier = Modifier, onFullAmountChange: (BigDecimal) -> Unit, isActive: Boolean, @@ -557,13 +572,13 @@ private fun WithoutSkontoSection( .padding(top = 16.dp), enabled = isActive, colors = colors.amountFieldColors, - amount = amount, + amount = amount.amount, onValueChange = onFullAmountChange, label = stringResource(id = R.string.gbs_skonto_section_without_discount_field_amount_hint), trailingContent = { AnimatedVisibility(visible = isActive) { Text( - text = currencyCode, + text = amount.currencyCode, style = GiniTheme.typography.subtitle1, ) } @@ -575,9 +590,8 @@ private fun WithoutSkontoSection( @Composable private fun FooterSection( - totalAmount: BigDecimal, + totalAmount: SkontoData.Amount, discountValue: BigDecimal, - currency: String, colors: SkontoFooterSectionColors, isBottomNavigationBarEnabled: Boolean, isSkontoSectionActive: Boolean, @@ -588,12 +602,13 @@ private fun FooterSection( customBottomNavBarAdapter: InjectedViewAdapterInstance?, ) { val animatedTotalAmount by animateFloatAsState( - targetValue = totalAmount.toFloat(), label = "totalAmount" + targetValue = totalAmount.amount.toFloat(), label = "totalAmount" ) val animatedDiscountAmount by animateFloatAsState( targetValue = discountValue.toFloat(), label = "discountAmount" ) - val totalPriceText = "${currencyFormatterWithoutSymbol().format(animatedTotalAmount)} $currency" + val totalPriceText = + "${currencyFormatterWithoutSymbol().format(animatedTotalAmount)} ${totalAmount.currencyCode}" val discountLabelText = stringResource( id = R.string.gbs_skonto_section_footer_label_discount, animatedDiscountAmount.formatAsDiscountPercentage() @@ -736,10 +751,9 @@ private fun Float.formatAsDiscountPercentage(): String { private val previewState = SkontoFragmentContract.State.Ready( isSkontoSectionActive = true, paymentInDays = 14, - discountValue = BigDecimal("3"), - skontoAmount = BigDecimal("97"), + discountAmount = BigDecimal("3"), + skontoAmount = SkontoData.Amount(BigDecimal("97"), "EUR"), discountDueDate = LocalDate.now(), - fullAmount = BigDecimal("100"), - totalAmount = BigDecimal("97"), - currency = "EUR" + fullAmount = SkontoData.Amount(BigDecimal("100"), "EUR"), + totalAmount = SkontoData.Amount(BigDecimal("97"), "EUR"), ) \ No newline at end of file diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt index 167aea178..e6e4fbfad 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt @@ -1,22 +1,20 @@ package net.gini.android.bank.sdk.capture.skonto +import net.gini.android.bank.sdk.capture.skonto.model.SkontoData import java.math.BigDecimal import java.time.LocalDate -object SkontoFragmentContract { +internal object SkontoFragmentContract { sealed class State { - object Idle : State() - data class Ready( val isSkontoSectionActive: Boolean, val paymentInDays: Int, - val discountValue: BigDecimal, - val skontoAmount: BigDecimal, + val discountAmount: BigDecimal, + val skontoAmount: SkontoData.Amount, val discountDueDate: LocalDate, - val fullAmount: BigDecimal, - val totalAmount: BigDecimal, - val currency: String, + val fullAmount: SkontoData.Amount, + val totalAmount: SkontoData.Amount, ) : State() } diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt index f9e4757ee..202060a5a 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt @@ -4,73 +4,67 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import net.gini.android.bank.sdk.capture.skonto.model.SkontoData import java.math.BigDecimal import java.math.RoundingMode import java.time.LocalDate -class SkontoFragmentViewModel : ViewModel() { +internal class SkontoFragmentViewModel( + private val data: SkontoData, +) : ViewModel() { val stateFlow: MutableStateFlow = - MutableStateFlow( - createInitalState( - skontoAmount = BigDecimal("97"), - fullAmount = BigDecimal("100"), - paymentInDays = 14, - isSkontoSectionActive = true, - currency = "EUR", - discountDueDate = LocalDate.now() - ) - ) + MutableStateFlow(createInitalState(data)) private fun createInitalState( - skontoAmount: BigDecimal, - fullAmount: BigDecimal, - paymentInDays: Int, - isSkontoSectionActive: Boolean = true, - currency: String = "EUR", - discountDueDate: LocalDate, + data: SkontoData, ): SkontoFragmentContract.State.Ready { - val totalAmount = if (isSkontoSectionActive) skontoAmount else fullAmount - val discount = calculateDiscount(skontoAmount, fullAmount) + val isSkontoSectionActive = true + + val totalAmount = + if (isSkontoSectionActive) data.skontoAmountToPay else data.fullAmountToPay + val discount = data.skontoPercentageDiscounted return SkontoFragmentContract.State.Ready( isSkontoSectionActive = true, - paymentInDays = paymentInDays, - discountValue = discount, - skontoAmount = skontoAmount, - discountDueDate = discountDueDate, - fullAmount = fullAmount, + paymentInDays = data.skontoRemainingDays, + discountAmount = discount, + skontoAmount = data.skontoAmountToPay, + discountDueDate = data.skontoDueDate, + fullAmount = data.fullAmountToPay, totalAmount = totalAmount, - currency = currency, ) } fun onSkontoActiveChanged(newValue: Boolean) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch val totalAmount = if (newValue) currentState.skontoAmount else currentState.fullAmount - val discount = calculateDiscount(currentState.skontoAmount, currentState.fullAmount) + val discount = + calculateDiscount(currentState.skontoAmount.amount, currentState.fullAmount.amount) stateFlow.emit( currentState.copy( isSkontoSectionActive = newValue, totalAmount = totalAmount, - discountValue = discount + discountAmount = discount ) ) } fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch - val discount = calculateDiscount(newValue, currentState.fullAmount) - val totalAmount = - if (currentState.isSkontoSectionActive) newValue else currentState.fullAmount + val discount = calculateDiscount(newValue, currentState.fullAmount.amount) + val totalAmount = if (currentState.isSkontoSectionActive) + newValue + else + currentState.fullAmount.amount stateFlow.emit( currentState.copy( - skontoAmount = newValue, - discountValue = discount, - totalAmount = totalAmount, + skontoAmount = currentState.skontoAmount.copy(amount = newValue), + discountAmount = discount, + totalAmount = currentState.totalAmount.copy(amount = totalAmount), ) ) } @@ -83,8 +77,10 @@ class SkontoFragmentViewModel : ViewModel() { fun onFullAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch val totalAmount = - if (currentState.isSkontoSectionActive) currentState.skontoAmount else newValue - val discount = currentState.discountValue + if (currentState.isSkontoSectionActive) currentState.skontoAmount.amount else newValue + + val discount = currentState.discountAmount + val skontoAmount = newValue.minus( newValue.multiply( // full_amount - (full_amount * (discount / 100)) discount.divide(BigDecimal("100"), 2, RoundingMode.HALF_UP) @@ -93,9 +89,9 @@ class SkontoFragmentViewModel : ViewModel() { stateFlow.emit( currentState.copy( - skontoAmount = skontoAmount, - fullAmount = newValue, - totalAmount = totalAmount + skontoAmount = currentState.skontoAmount.copy(amount = skontoAmount), + fullAmount = currentState.fullAmount.copy(amount = newValue), + totalAmount = currentState.totalAmount.copy(amount = totalAmount) ) ) } diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoValidator.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoValidator.kt deleted file mode 100644 index bae197155..000000000 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoValidator.kt +++ /dev/null @@ -1,85 +0,0 @@ -package net.gini.android.bank.sdk.capture.skonto - -import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceException.* -import net.gini.android.bank.sdk.capture.skonto.SkontoException.* -import net.gini.android.capture.network.model.GiniCaptureCompoundExtraction - -internal typealias Validate = (compoundExtractions: Map) -> Unit - -/** - * It checks that the compound extractions contain valid skonto objects which can be used to show the Skonto screen. - */ -class SkontoValidator { - - companion object { - - /** - * Checks that the compound extractions contain valid line items. - * - * In case it's not valid an appropriate [SkontoException] subclass will be thrown. - * - * @param compoundExtractions a map of [GiniCaptureCompoundExtraction]s - * @throws SkontoMissingException if skontoDiscounts are missing from the compound extractions - * @throws DueDateMissingException if skontoDueDate is missing from skontoDiscaounts object - * @throws AmountToPayMissingException if skontoAmountToPay is missing from skontoDiscaounts object - * @throws RemainingDaysMissingException if skontoRemainingDays is missing from skontoDiscaounts object - * @throws PercentageDiscountedMissingException if skontoPercentageDiscounted is missing from skontoDiscaounts object - */ - @JvmStatic - @Throws(Exception::class) - fun validate(compoundExtractions: Map) = listOf( - skontoDiscountsAvailable, - skontoDueDateAvailable, - skontoAmountToPayAvailable, - skontoRemainingDaysAvailable, - skontoPercentageDiscountedAvailable - ).forEach { it(compoundExtractions) } - } -} - -//mandatory -internal val skontoDiscountsAvailable: Validate = { compoundExtractions -> - if (!compoundExtractions.containsKey("skontoDiscounts")) { - throw SkontoMissingException() - } -} -//mandatory -internal val skontoDueDateAvailable: Validate = { compoundExtractions -> - if ((compoundExtractions["skontoDiscounts"]?.specificExtractionMaps?.all { it.containsKey("skontoDueDate") || it.containsKey("skontoDueDateCalculated") }) != true) { - throw DueDateMissingException() - } -} -//not mandatory -internal val skontoAmountDiscountedAvailable: Validate = { compoundExtractions -> - if ((compoundExtractions["skontoDiscounts"]?.specificExtractionMaps?.all { it.containsKey("skontoAmountDiscounted") || it.containsKey("skontoAmountDiscountedCalculated") }) != true) { - throw AmountDiscountedMissingException() - } -} -//mandatory -internal val skontoAmountToPayAvailable: Validate = { compoundExtractions -> - if ((compoundExtractions["skontoDiscounts"]?.specificExtractionMaps?.all { it.containsKey("skontoAmountToPay") || it.containsKey("skontoAmountToPayCalculated") }) != true) { - throw AmountToPayMissingException() - } -} - -//mandatory -internal val skontoRemainingDaysAvailable: Validate = { compoundExtractions -> - if ((compoundExtractions["skontoDiscounts"]?.specificExtractionMaps?.all { it.containsKey("skontoRemainingDays") || it.containsKey("skontoRemainingDaysCalculated") }) != true) { - throw RemainingDaysMissingException() - } -} - -//not mandatory -internal val skontoPaymentMethodAvailable: Validate = { compoundExtractions -> - if ((compoundExtractions["skontoDiscounts"]?.specificExtractionMaps?.all { it.containsKey("skontoPaymentMethod") }) != true) { - throw PaymentMethodMissingException() - } -} - -//mandatory -internal val skontoPercentageDiscountedAvailable: Validate = { compoundExtractions -> - if ((compoundExtractions["skontoDiscounts"]?.specificExtractionMaps?.all { it.containsKey("skontoPercentageDiscounted") || it.containsKey("skontoPercentageDiscountedCalculated") }) != true) { - throw PercentageDiscountedMissingException() - } -} - diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoData.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoData.kt new file mode 100644 index 000000000..79cebf63d --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoData.kt @@ -0,0 +1,51 @@ +package net.gini.android.bank.sdk.capture.skonto.model + +import java.io.Serializable +import java.math.BigDecimal +import java.time.LocalDate + +data class SkontoData( + val skontoPercentageDiscounted: BigDecimal, + val skontoPaymentMethod: SkontoPaymentMethod?, + val skontoAmountToPay: Amount, + val fullAmountToPay: Amount, + val skontoRemainingDays: Int, + val skontoDueDate: LocalDate, +) : Serializable { + + enum class SkontoPaymentMethod : Serializable { + Unspecified, Cash, PayPal + } + + data class Amount(val amount: BigDecimal, val currencyCode: String) : Serializable { + + companion object { + + /** + * Creates [Amount] from string in format `value:currency_code` or throws an Exception + */ + @Throws(IllegalArgumentException::class) + fun parse(amountStr: String): Amount { + val amountParts = amountStr.split(":").also { + if (it.size != 2) { + throw IllegalArgumentException( + "Invalid amount format for value: $amountStr. " + + "Should be `value:currency_code`" + ) + } + } + + val amount = runCatching { BigDecimal(amountParts.first()) }.getOrElse { + throw IllegalArgumentException( + "Invalid amount format for value: $amountStr. " + + "Can't convert `${amountParts.first()} to BigDecimal`" + ) + } + + val currencyCode = amountParts.last() + + return Amount(amount, currencyCode) + } + } + } +} diff --git a/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml b/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml index 3b45eb77c..6c9889d7c 100644 --- a/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml +++ b/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml @@ -98,6 +98,11 @@ + android:label="SkontoFragment"> + + + \ No newline at end of file