taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

PaymentManager.kt (11793B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.wallet.payment
     18 
     19 import android.util.Log
     20 import androidx.annotation.UiThread
     21 import androidx.lifecycle.LiveData
     22 import androidx.lifecycle.MutableLiveData
     23 import kotlinx.coroutines.CoroutineScope
     24 import kotlinx.coroutines.launch
     25 import kotlinx.serialization.Serializable
     26 import net.taler.common.Amount
     27 import net.taler.common.ContractInput
     28 import net.taler.common.ContractOutput
     29 import net.taler.common.ContractTerms
     30 import net.taler.common.TalerUtils.getLocalizedString
     31 import net.taler.wallet.main.TAG
     32 import net.taler.wallet.backend.BackendManager
     33 import net.taler.wallet.backend.TalerErrorInfo
     34 import net.taler.wallet.backend.WalletBackendApi
     35 import net.taler.wallet.balances.ScopeInfo
     36 import net.taler.wallet.donau.DonauInfo
     37 import net.taler.wallet.donau.GetDonauResponse
     38 import net.taler.wallet.exchanges.ExchangeManager
     39 import net.taler.wallet.payment.PayStatus.AlreadyPaid
     40 import net.taler.wallet.payment.PayStatus.InsufficientBalance
     41 import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse
     42 import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse
     43 import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse
     44 import org.json.JSONObject
     45 import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail
     46 import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail.PaymentPossible
     47 
     48 sealed class PayStatus {
     49     data object None : PayStatus()
     50     data object Loading : PayStatus()
     51     data class Prepared(
     52         val transactionId: String,
     53         val contractTerms: ContractTerms,
     54     ) : PayStatus()
     55 
     56     data class Choices(
     57         val transactionId: String,
     58         val contractTerms: ContractTerms,
     59         val choices: List<PayChoiceDetails>,
     60         val defaultChoiceIndex: Int? = null,
     61     ) : PayStatus()
     62 
     63     data class Checked(
     64         val details: WalletTemplateDetails,
     65         val supportedCurrencies: List<String>,
     66     ) : PayStatus()
     67 
     68     data class InsufficientBalance(
     69         val transactionId: String,
     70         val contractTerms: ContractTerms,
     71         val amountRaw: Amount,
     72         val balanceDetails: PaymentInsufficientBalanceDetails,
     73     ) : PayStatus()
     74 
     75     data class AlreadyPaid(
     76         val transactionId: String,
     77     ) : PayStatus()
     78 
     79     data class Pending(
     80         val transactionId: String? = null,
     81         val error: TalerErrorInfo? = null,
     82     ) : PayStatus()
     83     data class Success(
     84         val transactionId: String,
     85         val automaticExecution: Boolean,
     86     ) : PayStatus()
     87 }
     88 
     89 data class PayChoiceDetails(
     90     val choiceIndex: Int,
     91     val amountRaw: Amount,
     92     val description: String? = null,
     93     val descriptionI18n: Map<String, String>? = null,
     94     val inputs: List<ContractInput>,
     95     val outputs: List<ContractOutput>,
     96     val details: ChoiceSelectionDetail,
     97 ) {
     98     val localizedDescription: String?
     99         get() = description?.let {
    100             getLocalizedString(descriptionI18n, it)
    101         }
    102 }
    103 
    104 @Serializable
    105 data class CheckPayTemplateResponse(
    106     val templateDetails: WalletTemplateDetails,
    107     val supportedCurrencies: List<String>,
    108 )
    109 
    110 class PaymentManager(
    111     private val api: WalletBackendApi,
    112     private val scope: CoroutineScope,
    113     private val exchangeManager: ExchangeManager,
    114 ) {
    115     private val mPayStatus = MutableLiveData<PayStatus>(PayStatus.None)
    116     internal val payStatus: LiveData<PayStatus> = mPayStatus
    117 
    118     suspend fun preparePay(url: String): String? {
    119         var transactionId: String? = null
    120         mPayStatus.value = PayStatus.Loading
    121         api.request("preparePayForUriV2", PreparePayV2Response.serializer()) {
    122             put("talerPayUri", url)
    123         }.onError {
    124             handleError("preparePayForUriV2", it)
    125         }.onSuccess { response ->
    126             transactionId = response.transactionId
    127         }
    128         return transactionId
    129     }
    130 
    131     @UiThread
    132     fun getPaymentChoices(
    133         transactionId: String,
    134         onSuccess: () -> Unit,
    135     ) = scope.launch {
    136         api.request("getChoicesForPayment", GetChoicesForPaymentResponse.serializer()) {
    137             put("transactionId", transactionId)
    138         }.onSuccess { res ->
    139             if (res.automaticExecution == true && res.automaticExecutableIndex != null) {
    140                 confirmPay(transactionId, res.automaticExecutableIndex, automaticExecution = true)
    141                 return@onSuccess
    142             }
    143 
    144             mPayStatus.value = PayStatus.Choices(
    145                 transactionId = transactionId,
    146                 contractTerms = res.contractTerms,
    147                 defaultChoiceIndex = res.defaultChoiceIndex,
    148                 choices = res.choices.map { choice ->
    149                     val spec = exchangeManager.getSpecForCurrency(
    150                         choice.amountRaw.currency,
    151                         res.contractTerms.exchanges.map {
    152                             ScopeInfo.Exchange(choice.amountRaw.currency, it.url)
    153                         },
    154                     ) ?: exchangeManager.getSpecForCurrency(choice.amountRaw.currency)
    155 
    156                     when (choice) {
    157                         is PaymentPossible -> {
    158                             choice.copy(
    159                                 amountRaw = choice.amountRaw.withSpec(spec),
    160                                 amountEffective = choice.amountEffective.withSpec(spec),
    161                             )
    162                         }
    163 
    164                         is ChoiceSelectionDetail.InsufficientBalance -> {
    165                             choice.copy(amountRaw = choice.amountRaw.withSpec(spec))
    166                         }
    167                     }
    168                 }.mapIndexed { i, choice ->
    169                     PayChoiceDetails(
    170                         choiceIndex = i,
    171                         description = choice.description,
    172                         descriptionI18n = choice.descriptionI18n,
    173                         amountRaw = choice.amountRaw,
    174                         inputs = (res.contractTerms as? ContractTerms.V1)
    175                             ?.choices?.get(i)?.inputs ?: listOf(),
    176                         outputs = (res.contractTerms as? ContractTerms.V1)
    177                             ?.choices?.get(i)?.outputs ?: listOf(),
    178                         details = choice,
    179                     )
    180                 }.filter {
    181                     // Hide auto executable choice
    182                     res.automaticExecutableIndex != it.choiceIndex
    183                 }.sortedWith(
    184                     compareByDescending<PayChoiceDetails> {
    185                         it.choiceIndex == res.defaultChoiceIndex
    186                     }.thenByDescending {
    187                         it.details is PaymentPossible
    188                     }.thenByDescending {
    189                         it.amountRaw.toString()
    190                     },
    191                 ),
    192             )
    193 
    194             onSuccess()
    195         }.onError { error ->
    196             handleError("getChoicesForPayment", error)
    197         }
    198     }
    199 
    200     fun confirmPay(
    201         transactionId: String,
    202         choiceIndex: Int? = null,
    203         automaticExecution: Boolean = false,
    204         useDonau: Boolean = false,
    205     ) = scope.launch {
    206         mPayStatus.postValue(PayStatus.Loading)
    207         api.request("confirmPay", ConfirmPayResult.serializer()) {
    208             choiceIndex?.let { put("choiceIndex", it) }
    209             put("transactionId", transactionId)
    210             put("useDonau", useDonau)
    211         }.onError {
    212             handleError("confirmPay", it)
    213         }.onSuccess { response ->
    214             mPayStatus.postValue(when (response) {
    215                 is ConfirmPayResult.Done -> PayStatus.Success(
    216                     transactionId = response.transactionId,
    217                     automaticExecution = automaticExecution,
    218                 )
    219                 is ConfirmPayResult.Pending -> PayStatus.Pending(
    220                     transactionId = response.transactionId,
    221                     error = response.lastError,
    222                 )
    223             })
    224         }
    225     }
    226 
    227     suspend fun checkDonauForChoice(
    228         choiceDetails: PayChoiceDetails
    229     ): DonauStatus {
    230         val taxReceipt = (choiceDetails.outputs)
    231             .find { it is ContractOutput.TaxReceipt }
    232                 as ContractOutput.TaxReceipt?
    233 
    234         return if (taxReceipt != null) {
    235             var donauInfo: DonauInfo? = null
    236             api.request("getDonau", GetDonauResponse.serializer())
    237                 .onSuccess { donauInfo = it.currentDonauInfo }
    238 
    239             if (donauInfo == null) {
    240                 DonauStatus.Unset(taxReceipt.donauUrls.distinct())
    241             } else if (taxReceipt.donauUrls.contains(donauInfo!!.donauBaseUrl)) {
    242                 DonauStatus.Available
    243             } else {
    244                 DonauStatus.Mismatch(
    245                     donauInfo = donauInfo!!,
    246                     donauUrls = taxReceipt.donauUrls.distinct(),
    247                 )
    248             }
    249         } else {
    250             DonauStatus.Unavailable
    251         }
    252     }
    253 
    254     fun checkPayForTemplate(url: String) = scope.launch {
    255         mPayStatus.value = PayStatus.Loading
    256         api.request("checkPayForTemplate", CheckPayTemplateResponse.serializer()) {
    257             put("talerPayTemplateUri", url)
    258         }.onError {
    259             handleError("checkPayForTemplate", it)
    260         }.onSuccess { response ->
    261             if (response.templateDetails.templateContract.templateType == TemplateType.Paivana) {
    262                 scope.launch { preparePayForTemplate(url, TemplateParams()) }
    263             } else {
    264                 mPayStatus.value = PayStatus.Checked(
    265                     details = response.templateDetails,
    266                     supportedCurrencies = response.supportedCurrencies,
    267                 )
    268             }
    269         }
    270     }
    271 
    272     fun preparePayForTemplate(url: String, params: TemplateParams) = scope.launch {
    273         mPayStatus.value = PayStatus.Loading
    274         api.request("preparePayForTemplate", PreparePayResponse.serializer()) {
    275             put("talerPayTemplateUri", url)
    276             put("templateParams", JSONObject(BackendManager.json.encodeToString(params)))
    277         }.onError {
    278             handleError("preparePayForTemplate", it)
    279         }.onSuccess { response ->
    280             mPayStatus.value = when (response) {
    281                 is PaymentPossibleResponse -> response.toPayStatusPrepared()
    282                 is PreparePayResponse.ChoiceSelection -> response.toPayStatusPrepared()
    283 
    284                 is InsufficientBalanceResponse -> InsufficientBalance(
    285                     transactionId = response.transactionId,
    286                     contractTerms = response.contractTerms,
    287                     amountRaw = response.amountRaw,
    288                     balanceDetails = response.balanceDetails,
    289                 )
    290 
    291                 is AlreadyConfirmedResponse -> AlreadyPaid(
    292                     transactionId = response.transactionId,
    293                 )
    294             }
    295         }
    296     }
    297 
    298     @UiThread
    299     fun resetPayStatus() {
    300         mPayStatus.postValue(PayStatus.None)
    301     }
    302 
    303     private fun handleError(operation: String, error: TalerErrorInfo) {
    304         Log.e(TAG, "got $operation error result $error")
    305         mPayStatus.postValue(PayStatus.Pending(error = error))
    306     }
    307 
    308 }