RefundFragment.kt

/*
 * This file is part of GNU Taler
 * (C) 2020 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.merchantpos.refund

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels

import net.taler.common.Amount
import net.taler.common.AmountParserException
import net.taler.common.CurrencySpecification
import net.taler.merchantlib.OrderHistoryEntry
import net.taler.merchantpos.MainActivity
import net.taler.merchantpos.MainViewModel
import net.taler.merchantpos.PosDestination
import net.taler.merchantpos.R
import net.taler.merchantpos.checkout.Checkout
import net.taler.merchantpos.checkout.METHOD_CASH
import net.taler.merchantpos.checkout.shown
import net.taler.merchantpos.compose.PosTheme
import net.taler.merchantpos.history.OrderDetailsResult
import net.taler.merchantpos.showPosError

class RefundFragment : Fragment()
{
    private val model: MainViewModel by activityViewModels()
    private val refundManager by lazy { model.refundManager }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ) = ComposeView(requireContext()).apply {
        setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
        setContent {
            val item = refundManager.toBeRefunded
            if (item == null)
            {
                LaunchedEffect(Unit) { (requireActivity() as MainActivity).navigateBack() }
                return@setContent
            }
            var refresh by remember { mutableIntStateOf(0) }
            val details by produceState<OrderDetailsResult>(OrderDetailsResult.Loading, item.orderId, refresh) {
                value = OrderDetailsResult.Loading
                value = model.historyManager.fetchOrderDetails(item.orderId)
            }
            val response = (details as? OrderDetailsResult.Success)?.response
            if (response == null)
            {
                PosTheme {
                    Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
                        if (details is OrderDetailsResult.Loading)
                        {
                            LinearProgressIndicator(Modifier.fillMaxWidth())
                        }
                        else
                        {
                            Text(stringResource(R.string.order_details_load_error))
                            Button(onClick = { refresh++ }) { Text(stringResource(R.string.order_details_retry)) }
                        }
                    }
                }
                return@setContent
            }
            val currentItem = item.withPaymentDetails(response)
            val checkouts by model.checkoutManager.checkouts.observeAsState(emptyList())
            val submitting by refundManager.submitting.observeAsState(false)
            val result by refundManager.refundResult.observeAsState()
            val checkout = checkouts.find { it.backendOrderId == item.orderId }
            PosTheme {
                RefundScreen(
                    currentItem,
                    model.configManager.currencySpec,
                    checkout = checkout,
                    preferTaler = refundManager.preferTaler,
                    submitting = submitting,
                    onAbort = { (requireActivity() as MainActivity).navigateBack() },
                    onRefund = { choice, amount, reason ->
                        if (choice.paymentId == null)
                        {
                            refundManager.refund(currentItem, amount, reason)
                        }
                        else if (checkout != null)
                        {
                            refundManager.recordExternalRefund(
                                model.checkoutManager,
                                currentItem,
                                checkout.id,
                                choice.paymentId,
                                amount,
                                reason,
                            )
                        }
                    },
                )
                (result as? RefundResult.ExternalCompleted)?.let { completed ->
                    AlertDialog(
                        onDismissRequest = {},
                        title = { Text(stringResource(R.string.refund_recorded)) },
                        text = {
                            Text(
                                if (completed.warning ==
                                    null)
                                {
                                    stringResource(R.string.refund_recorded_message)
                                }
                                else
                                {
                                    stringResource(R.string.refund_recorded_local, completed.warning)
                                },
                            )
                        },
                        confirmButton = {
                            TextButton(
                                onClick = {
                                    refundManager.completeRefund()
                                    model.historyManager.fetchHistory()
                                    (requireActivity() as MainActivity).navigateBack()
                                }
                            ) { Text(stringResource(android.R.string.ok)) }
                        },
                    )
                }
            }
        }
    }

    override fun onViewCreated(
        view: View,
        savedInstanceState: Bundle?,
    )
    {
        refundManager.refundResult.observe(viewLifecycleOwner) { result ->
            when (result)
            {
                is RefundResult.Error ->
                {
                    requireActivity().showPosError(R.string.refund_error_backend, result.msg)
                    refundManager.clearResult()
                }

                RefundResult.PastDeadline ->
                {
                    requireActivity().showPosError(R.string.refund_error_deadline)
                    refundManager.clearResult()
                }

                RefundResult.AlreadyRefunded ->
                {
                    requireActivity().showPosError(R.string.refund_error_already_refunded)
                    refundManager.clearResult()
                }

                is RefundResult.Success ->
                {
                    (requireActivity() as MainActivity).navigateTo(PosDestination.RefundUri)
                }

                else ->
                {
                    Unit
                }
            }
        }
    }
}

private data class RefundChoice(
    val key: String,
    val paymentId: String?,
    val label: String,
    val remaining: Amount,
    val enabled: Boolean,
    val cash: Boolean = false,
    val reference: String? = null,
)

@Composable
private fun RefundScreen(
    item: OrderHistoryEntry,
    currencySpec: CurrencySpecification?,
    initialReason: String? = null,
    checkout: Checkout? = null,
    preferTaler: Boolean = false,
    submitting: Boolean = false,
    onAbort: () -> Unit,
    onRefund: (RefundChoice, Amount, String) -> Unit,
)
{
    val cashLabel = stringResource(R.string.payment_option_cash)
    val talerLabel = stringResource(R.string.payment_option_taler)
    val choices =
        item.amountExternal.orEmpty().map { payment ->
            val remaining = item.externalRefundRemaining(payment, checkout)
            val local = checkout?.payments?.find { it.id == payment.id }
            RefundChoice(
                payment.id,
                payment.id,
                if (payment.method.equals(METHOD_CASH, true))
                {
                    cashLabel
                }
                else
                {
                    payment.method.replaceFirstChar { it.uppercase() }
                },
                remaining,
                item.paid && !remaining.isZero() && local != null,
                payment.method.equals(METHOD_CASH, true),
                payment.transactionCode,
            )
        } + RefundChoice("taler", null, talerLabel, item.talerRefundRemaining(), !item.talerRefundRemaining().isZero())
    var selected by rememberSaveable(item.orderId) {
        mutableStateOf(
            (if (preferTaler) choices.find { it.paymentId == null && it.enabled } else null)?.key
                ?: choices.firstOrNull { it.enabled }?.key ?: choices.last().key,
        )
    }
    val choice = choices.first { it.key == selected }
    var amountText by rememberSaveable(selected, choice.remaining.toString()) {
        mutableStateOf(choice.remaining.withSpec(currencySpec).amountStr)
    }
    var reason by rememberSaveable(item.orderId) { mutableStateOf(initialReason ?: "") }
    var error by remember { mutableStateOf<String?>(null) }
    var reviewAmount by remember { mutableStateOf<Amount?>(null) }
    val focusManager = LocalFocusManager.current
    val invalid = stringResource(R.string.refund_error_invalid_amount)
    val zero = stringResource(R.string.refund_error_zero)
    val tooMuch = stringResource(R.string.refund_error_max_amount, choice.remaining.shown(currencySpec))
    BackHandler(submitting) { }
    PosTheme {
        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
            Column(
                Modifier
                    .widthIn(max = 1000.dp)
                    .fillMaxWidth()
                    .imePadding()
                    .verticalScroll(rememberScrollState())
                    .padding(20.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp),
            ) {
                Text(item.summary, style = MaterialTheme.typography.titleSmall)
                Text(item.orderId, style = MaterialTheme.typography.bodySmall)
                Text(stringResource(R.string.refund_choose_method), style = MaterialTheme.typography.titleMedium)
                BoxWithConstraints(Modifier.fillMaxWidth()) {
                    val optionWidth = if (maxWidth < 600.dp) maxWidth else (maxWidth - 24.dp) / 3
                    FlowRow(
                        horizontalArrangement = Arrangement.spacedBy(12.dp),
                        verticalArrangement = Arrangement.spacedBy(12.dp),
                    ) {
                        choices.forEach { option ->
                            OutlinedCard(
                                onClick = {
                                    if (option.enabled)
                                    {
                                        selected = option.key
                                        error = null
                                    }
                                },
                                enabled = option.enabled && !submitting,
                                colors =
                                    CardDefaults.outlinedCardColors(
                                        containerColor =
                                            if (selected == option.key)
                                            {
                                                MaterialTheme.colorScheme.primaryContainer
                                            }
                                            else
                                            {
                                                MaterialTheme.colorScheme.surface
                                            },
                                    ),
                                modifier = Modifier.width(optionWidth),
                            ) {
                                Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
                                    Row(
                                        Modifier.fillMaxWidth(),
                                        horizontalArrangement = Arrangement.spacedBy(12.dp),
                                        verticalAlignment = Alignment.CenterVertically,
                                    ) {
                                        Box(Modifier.weight(1f)) {
                                            if (option.paymentId ==
                                                null)
                                            {
                                                Image(
                                                    painterResource(R.drawable.ic_taler_logo_brand),
                                                    talerLabel,
                                                    Modifier.size(54.dp, 24.dp),
                                                )
                                            }
                                            else
                                            {
                                                Text(option.label, style = MaterialTheme.typography.labelLarge)
                                            }
                                        }
                                        Text(
                                            option.remaining.shown(currencySpec),
                                            style = MaterialTheme.typography.bodyMedium,
                                        )
                                    }
                                    Text(
                                        stringResource(
                                            if (option.enabled)
                                            {
                                                R.string.refund_available
                                            }
                                            else
                                            {
                                                R.string.refund_unavailable
                                            },
                                        ),
                                        style = MaterialTheme.typography.bodySmall,
                                    )
                                    option.reference?.let { Text(it, style = MaterialTheme.typography.bodySmall) }
                                }
                            }
                        }
                    }
                }
                Text(
                    stringResource(
                        when
                        {
                            !choice.enabled -> R.string.refund_unavailable_hint
                            choice.paymentId == null -> R.string.refund_taler_hint
                            choice.cash -> R.string.refund_cash_hint
                            else -> R.string.refund_card_hint
                        },
                    ),
                    style = MaterialTheme.typography.bodyMedium,
                )
                OutlinedTextField(
                    amountText,
                    {
                        amountText = it
                        error = null
                    },
                    Modifier.fillMaxWidth(),
                    enabled = choice.enabled && !submitting,
                    label = { Text(stringResource(R.string.refund_amount)) },
                    supportingText = error?.let { { Text(it) } },
                    isError = error != null,
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
                    singleLine = true,
                )
                OutlinedTextField(
                    reason,
                    { reason = it },
                    Modifier.fillMaxWidth(),
                    enabled = choice.enabled && !submitting,
                    label = { Text(stringResource(R.string.refund_reason)) },
                )
                FlowRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
                    Button(
                        enabled = choice.enabled && !submitting,
                        onClick = {
                            val value =
                                try
                                {
                                    Amount.fromString(item.amount.currency, amountText.trim()).withSpec(currencySpec)
                                }
                                catch (
                                    _: AmountParserException,
                                )
                                {
                                    error = invalid
                                    null
                                }
                            if (value != null)
                            {
                                when
                                {
                                    value.isZero() -> {
                                        error = zero
                                    }

                                    value > choice.remaining -> {
                                        error = tooMuch
                                    }

                                    else ->
                                    {
                                        focusManager.clearFocus()
                                        reviewAmount = value
                                    }
                                }
                            }
                        }
                    ) {
                        Text(
                            stringResource(
                                if (choice.paymentId ==
                                    null)
                                {
                                    R.string.refund_taler_button
                                }
                                else
                                {
                                    R.string.refund_record_external
                                },
                            ),
                        )
                    }
                    OutlinedButton(
                        onClick = onAbort,
                        enabled = !submitting,
                    ) { Text(stringResource(android.R.string.cancel)) }
                }
                if (submitting) LinearProgressIndicator(Modifier.fillMaxWidth())
            }
        }
        reviewAmount?.let { amount ->
            AlertDialog(
                onDismissRequest = { reviewAmount = null },
                title = { Text(stringResource(R.string.refund_confirm)) },
                text = {
                    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
                        Text("${choice.label}: ${amount.shown(currencySpec)}")
                        if (reason.isNotBlank()) Text(reason)
                        Text(
                            stringResource(
                                if (choice.paymentId == null)
                                {
                                    R.string.refund_taler_hint
                                }
                                else if (choice.cash)
                                {
                                    R.string.refund_cash_confirm
                                }
                                else
                                {
                                    R.string.refund_card_confirm
                                },
                            ),
                        )
                    }
                },
                confirmButton = {
                    Button(
                        onClick = {
                            reviewAmount = null
                            onRefund(choice, amount, reason)
                        }
                    ) {
                        Text(
                            stringResource(
                                if (choice.paymentId ==
                                    null)
                                {
                                    R.string.refund_confirm
                                }
                                else
                                {
                                    R.string.refund_returned_confirm
                                },
                            ),
                        )
                    }
                },
                dismissButton = {
                    TextButton(
                        onClick = { reviewAmount = null },
                    ) { Text(stringResource(android.R.string.cancel)) }
                },
            )
        }
    }
}

@Composable
internal fun RefundScreenContent(
    item: OrderHistoryEntry,
    currencySpec: CurrencySpecification?,
    initialReason: String? = null,
    checkout: Checkout? = null,
)
{
    RefundScreen(item, currencySpec, initialReason, checkout = checkout, onAbort = {}, onRefund = { _, _, _ -> })
}