/*
* 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
*/
package net.taler.wallet.transactions
import android.content.Context
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Transient
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonElement
import net.taler.common.Amount
import net.taler.common.ContractMerchant
import net.taler.common.ContractProduct
import net.taler.common.Timestamp
import net.taler.wallet.R
import net.taler.wallet.TAG
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.common.CurrencySpecification
import net.taler.wallet.cleanExchange
import net.taler.wallet.refund.RefundPaymentInfo
import net.taler.wallet.transactions.TransactionMajorState.None
import net.taler.wallet.transactions.TransactionMajorState.Pending
import net.taler.wallet.transactions.WithdrawalDetails.ManualTransfer
import net.taler.wallet.transactions.WithdrawalDetails.TalerBankIntegrationApi
import java.util.UUID
@Serializable
data class Transactions(
@Serializable(with = TransactionListSerializer::class)
val transactions: List,
)
class TransactionListSerializer : KSerializer> {
private val serializer = ListSerializer(TransactionSerializer())
override val descriptor: SerialDescriptor = serializer.descriptor
override fun deserialize(decoder: Decoder): List {
return decoder.decodeSerializableValue(serializer)
}
override fun serialize(encoder: Encoder, value: List) {
throw NotImplementedError()
}
}
class TransactionSerializer : KSerializer {
private val serializer = Transaction.serializer()
override val descriptor: SerialDescriptor = serializer.descriptor
private val jsonSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
override fun deserialize(decoder: Decoder): Transaction {
return try {
decoder.decodeSerializableValue(serializer)
} catch (e: SerializationException) {
Log.e(TAG, "Error deserializing transaction.", e)
DummyTransaction(
transactionId = UUID.randomUUID().toString(),
timestamp = Timestamp.now(),
error = TalerErrorInfo(
code = TalerErrorCode.UNKNOWN,
message = e.message,
extra = decoder.decodeSerializableValue(jsonSerializer)
),
)
}
}
override fun serialize(encoder: Encoder, value: Transaction) {
throw NotImplementedError()
}
}
@Serializable
sealed class Transaction {
abstract val transactionId: String
abstract val timestamp: Timestamp
abstract val txState: TransactionState
abstract val txActions: List
abstract val error: TalerErrorInfo?
abstract val amountRaw: Amount
abstract val amountEffective: Amount
@get:DrawableRes
abstract val icon: Int
@get:IdRes
abstract val detailPageNav: Int
abstract val amountType: AmountType
abstract fun getTitle(context: Context): String
@get:StringRes
abstract val generalTitleRes: Int
}
@Serializable
enum class TransactionAction {
// Common States
@SerialName("delete")
Delete,
@SerialName("suspend")
Suspend,
@SerialName("resume")
Resume,
@SerialName("abort")
Abort,
@SerialName("fail")
Fail,
@SerialName("retry")
Retry,
}
sealed class AmountType {
object Positive : AmountType()
object Negative : AmountType()
object Neutral : AmountType()
}
@Serializable
@SerialName("withdrawal")
class TransactionWithdrawal(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val kycUrl: String? = null,
val exchangeBaseUrl: String,
val withdrawalDetails: WithdrawalDetails,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_withdrawal
override val detailPageNav = R.id.action_nav_transactions_detail_withdrawal
@Transient
override val amountType = AmountType.Positive
override fun getTitle(context: Context) = cleanExchange(exchangeBaseUrl)
override val generalTitleRes = R.string.withdraw_title
val confirmed: Boolean
get() = txState.major != Pending && (
(withdrawalDetails is TalerBankIntegrationApi && withdrawalDetails.confirmed) ||
withdrawalDetails is ManualTransfer
)
}
@Serializable
sealed class WithdrawalDetails {
@Serializable
@SerialName("manual-transfer")
class ManualTransfer(
val exchangeCreditAccountDetails: List? = null,
) : WithdrawalDetails()
@Serializable
@SerialName("taler-bank-integration-api")
class TalerBankIntegrationApi(
/**
* Set to true if the bank has confirmed the withdrawal, false if not.
* An unconfirmed withdrawal usually requires user-input
* and should be highlighted in the UI.
* See also bankConfirmationUrl below.
*/
val confirmed: Boolean,
/**
* If the withdrawal is unconfirmed, this can include a URL for user-initiated confirmation.
*/
val bankConfirmationUrl: String? = null,
) : WithdrawalDetails()
}
@Serializable
data class WithdrawalExchangeAccountDetails (
/**
* Payto URI to credit the exchange.
*
* Depending on whether the (manual!) withdrawal is accepted or just
* being checked, this already includes the subject with the
* reserve public key.
*/
val paytoUri: String,
/**
* Status that indicates whether the account can be used
* by the user to send funds for a withdrawal.
*
* ok: account should be shown to the user
* error: account should not be shown to the user, UIs might render the error (in conversionError),
* especially in dev mode.
*/
val status: Status,
/**
* Transfer amount. Might be in a different currency than the requested
* amount for withdrawal.
*
* Redundant with the amount in paytoUri, just included to avoid parsing.
*/
val transferAmount: Amount? = null,
/**
* Currency specification for the external currency.
*
* Only included if this account requires a currency conversion.
*/
val currencySpecification: CurrencySpecification? = null,
/**
* Further restrictions for sending money to the
* exchange.
*/
val creditRestrictions: List? = null,
/**
* Label given to the account or the account's bank by the exchange.
*/
val bankLabel: String? = null,
val priority: Int? = null,
) {
@Serializable
enum class Status {
@SerialName("ok")
Ok,
@SerialName("error")
Error;
}
}
@Serializable
sealed class AccountRestriction {
@Serializable
@SerialName("deny")
data object DenyAllAccount: AccountRestriction()
@Serializable
@SerialName("regex")
data class RegexAccount(
// Regular expression that the payto://-URI of the
// partner account must follow. The regular expression
// should follow posix-egrep, but without support for character
// classes, GNU extensions, back-references or intervals. See
// https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
// for a description of the posix-egrep syntax. Applications
// may support regexes with additional features, but exchanges
// must not use such regexes.
@SerialName("payto_regex")
val paytoRegex: String,
// Hint for a human to understand the restriction
// (that is hopefully easier to comprehend than the regex itself).
@SerialName("human_hint")
val humanHint: String,
// Map from IETF BCP 47 language tags to localized
// human hints.
@SerialName("human_hint_i18n")
val humanHintI18n: Map? = null,
): AccountRestriction()
}
@Serializable
@SerialName("payment")
class TransactionPayment(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val info: TransactionInfo,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val posConfirmation: String? = null,
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
override val detailPageNav = R.id.action_nav_transactions_detail_payment
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context) = info.merchant.name
override val generalTitleRes = R.string.payment_title
}
@Serializable
class TransactionInfo(
val orderId: String,
val merchant: ContractMerchant,
val summary: String,
@SerialName("summary_i18n")
val summaryI18n: Map? = null,
val products: List = emptyList(),
val fulfillmentUrl: String? = null,
/**
* Message shown to the user after the payment is complete.
*/
val fulfillmentMessage: String? = null,
/**
* Map from IETF BCP 47 language tags to localized fulfillment messages
*/
val fulfillmentMessage_i18n: Map? = null,
)
@Serializable
@SerialName("refund")
class TransactionRefund(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val refundedTransactionId: String,
val paymentInfo: RefundPaymentInfo? = null,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_refund
override val detailPageNav = R.id.action_nav_transactions_detail_refund
@Transient
override val amountType = AmountType.Positive
override fun getTitle(context: Context): String {
val merchantName = paymentInfo?.merchant?.name ?: "null"
return context.getString(R.string.transaction_refund_from, merchantName)
}
override val generalTitleRes = R.string.refund_title
}
@Serializable
@SerialName("refresh")
class TransactionRefresh(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_refresh
override val detailPageNav = R.id.action_nav_transactions_detail_refresh
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_refresh)
}
override val generalTitleRes = R.string.transaction_refresh
}
@Serializable
@SerialName("deposit")
class TransactionDeposit(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val targetPaytoUri: String,
val depositGroupId: String,
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
override val detailPageNav = R.id.action_nav_transactions_detail_deposit
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_deposit)
}
override val generalTitleRes = R.string.transaction_deposit
}
@Serializable
data class PeerInfoShort(
val expiration: Timestamp? = null,
val summary: String? = null,
)
/**
* Debit because we paid someone's invoice.
*/
@Serializable
@SerialName("peer-pull-debit")
class TransactionPeerPullDebit(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val info: PeerInfoShort,
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
override val detailPageNav = R.id.nav_transactions_detail_peer
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_peer_pull_debit)
}
override val generalTitleRes = R.string.transaction_peer_pull_debit
}
/**
* Credit because someone paid for an invoice we created.
*/
@Serializable
@SerialName("peer-pull-credit")
class TransactionPeerPullCredit(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val info: PeerInfoShort,
val talerUri: String,
// val completed: Boolean, maybe
) : Transaction() {
override val icon = R.drawable.transaction_withdrawal
override val detailPageNav = R.id.nav_transactions_detail_peer
override val amountType get() = AmountType.Positive
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_peer_pull_credit)
}
override val generalTitleRes = R.string.transaction_peer_pull_credit
}
/**
* Debit because we sent money to someone.
*/
@Serializable
@SerialName("peer-push-debit")
class TransactionPeerPushDebit(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val info: PeerInfoShort,
val talerUri: String? = null,
// val completed: Boolean, definitely
) : Transaction() {
override val icon = R.drawable.ic_cash_usd_outline
override val detailPageNav = R.id.nav_transactions_detail_peer
@Transient
override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_peer_push_debit)
}
override val generalTitleRes = R.string.payment_title
}
/**
* We received money via a peer payment.
*/
@Serializable
@SerialName("peer-push-credit")
class TransactionPeerPushCredit(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
val exchangeBaseUrl: String,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val info: PeerInfoShort,
) : Transaction() {
override val icon = R.drawable.transaction_withdrawal
override val detailPageNav = R.id.nav_transactions_detail_peer
@Transient
override val amountType = AmountType.Positive
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_peer_push_credit)
}
override val generalTitleRes = R.string.transaction_peer_push_credit
}
/**
* A transaction to indicate financial loss due to denominations
* that became unusable for deposits.
*/
@Serializable
@SerialName("denom-loss")
class TransactionDenomLoss(
override val transactionId: String,
override val timestamp: Timestamp,
override val txState: TransactionState,
override val txActions: List,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
override val amountEffective: Amount,
val lossEventType: LossEventType,
): Transaction() {
override val icon: Int = R.drawable.transaction_loss
override val detailPageNav = R.id.nav_transactions_detail_loss
@Transient
override val amountType: AmountType = AmountType.Negative
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_denom_loss)
}
override val generalTitleRes: Int = R.string.transaction_denom_loss
}
@Serializable
enum class LossEventType {
@SerialName("denom-expired")
DenomExpired,
@SerialName("denom-vanished")
DenomVanished,
@SerialName("denom-unoffered")
DenomUnoffered
}
/**
* This represents a transaction that we can not parse for some reason.
*/
class DummyTransaction(
override val transactionId: String,
override val timestamp: Timestamp,
override val error: TalerErrorInfo,
) : Transaction() {
override val txState: TransactionState = TransactionState(None)
override val txActions: List = emptyList()
override val amountRaw: Amount = Amount.zero("TESTKUDOS")
override val amountEffective: Amount = Amount.zero("TESTKUDOS")
override val icon: Int = R.drawable.ic_bug_report
override val detailPageNav: Int = R.id.nav_transactions_detail_dummy
override val amountType: AmountType = AmountType.Neutral
override val generalTitleRes: Int = R.string.transaction_dummy_title
override fun getTitle(context: Context): String {
return context.getString(R.string.transaction_dummy_title)
}
}