diff options
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt')
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt | 354 |
1 files changed, 263 insertions, 91 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt index 68d0bc4..2bd204c 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -17,30 +17,93 @@ package net.taler.wallet.transactions import android.content.Context +import android.net.Uri +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(val transactions: List<Transaction>) +data class Transactions( + @Serializable(with = TransactionListSerializer::class) + val transactions: List<Transaction>, +) + +class TransactionListSerializer : KSerializer<List<Transaction>> { + private val serializer = ListSerializer(TransactionSerializer()) + override val descriptor: SerialDescriptor = serializer.descriptor + + override fun deserialize(decoder: Decoder): List<Transaction> { + return decoder.decodeSerializableValue(serializer) + } + + override fun serialize(encoder: Encoder, value: List<Transaction>) { + throw NotImplementedError() + } +} + +class TransactionSerializer : KSerializer<Transaction> { + + 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 extendedStatus: ExtendedStatus + abstract val txState: TransactionState + abstract val txActions: List<TransactionAction> abstract val error: TalerErrorInfo? abstract val amountRaw: Amount abstract val amountEffective: Amount @@ -59,33 +122,26 @@ sealed class Transaction { abstract val generalTitleRes: Int } -enum class ExtendedStatus { - @SerialName("pending") - Pending, - - @SerialName("done") - Done, - - @SerialName("aborting") - Aborting, - - @SerialName("aborted") - Aborted, +@Serializable +enum class TransactionAction { + // Common States + @SerialName("delete") + Delete, - @SerialName("suspended") - Suspended, + @SerialName("suspend") + Suspend, - @SerialName("failed") - Failed, + @SerialName("resume") + Resume, - @SerialName("kyc-required") - KycRequired, + @SerialName("abort") + Abort, - @SerialName("aml-required") - AmlRequired, + @SerialName("fail") + Fail, - @SerialName("deleted") - Deleted; + @SerialName("retry") + Retry, } sealed class AmountType { @@ -99,7 +155,9 @@ sealed class AmountType { class TransactionWithdrawal( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, + val kycUrl: String? = null, val exchangeBaseUrl: String, val withdrawalDetails: WithdrawalDetails, override val error: TalerErrorInfo? = null, @@ -115,7 +173,7 @@ class TransactionWithdrawal( override fun getTitle(context: Context) = cleanExchange(exchangeBaseUrl) override val generalTitleRes = R.string.withdraw_title val confirmed: Boolean - get() = extendedStatus != ExtendedStatus.Pending && ( + get() = txState.major != Pending && ( (withdrawalDetails is TalerBankIntegrationApi && withdrawalDetails.confirmed) || withdrawalDetails is ManualTransfer ) @@ -126,12 +184,7 @@ sealed class WithdrawalDetails { @Serializable @SerialName("manual-transfer") class ManualTransfer( - /** - * Payto URIs that the exchange supports. - * - * Already contains the amount and message. - */ - val exchangePaytoUris: List<String>, + val exchangeCreditAccountDetails: List<WithdrawalExchangeAccountDetails>? = null, ) : WithdrawalDetails() @Serializable @@ -153,16 +206,108 @@ sealed class 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<AccountRestriction>? = 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<String, String>? = null, + ): AccountRestriction() +} + +@Serializable @SerialName("payment") class TransactionPayment( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, val info: TransactionInfo, - val status: PaymentStatus, 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 @@ -180,7 +325,7 @@ class TransactionInfo( val summary: String, @SerialName("summary_i18n") val summaryI18n: Map<String, String>? = null, - val products: List<ContractProduct>, + val products: List<ContractProduct> = emptyList(), val fulfillmentUrl: String? = null, /** * Message shown to the user after the payment is complete. @@ -193,32 +338,14 @@ class TransactionInfo( ) @Serializable -enum class PaymentStatus { - @SerialName("aborted") - Aborted, - - @SerialName("failed") - Failed, - - @SerialName("paid") - Paid, - - @SerialName("accepted") - Accepted -} - -@Serializable @SerialName("refund") class TransactionRefund( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, val refundedTransactionId: String, - val info: TransactionInfo, - /** - * Part of the refund that couldn't be applied because the refund permissions were expired - */ - val amountInvalid: Amount? = null, + val paymentInfo: RefundPaymentInfo? = null, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, @@ -228,43 +355,18 @@ class TransactionRefund( @Transient override val amountType = AmountType.Positive - override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_refund_from, info.merchant.name) - } + override fun getTitle(context: Context) = paymentInfo?.merchant?.name ?: context.getString(R.string.transaction_refund) override val generalTitleRes = R.string.refund_title } @Serializable -@SerialName("tip") -class TransactionTip( - override val transactionId: String, - override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, - val merchantBaseUrl: String, - override val error: TalerErrorInfo? = null, - override val amountRaw: Amount, - override val amountEffective: Amount, -) : Transaction() { - override val icon = R.drawable.transaction_tip_accepted - override val detailPageNav = R.id.action_nav_transactions_detail_tip - - @Transient - override val amountType = AmountType.Positive - override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_tip_from, merchantBaseUrl) - } - - override val generalTitleRes = R.string.tip_title -} - -@Serializable @SerialName("refresh") class TransactionRefresh( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, - val exchangeBaseUrl: String, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, @@ -286,7 +388,8 @@ class TransactionRefresh( class TransactionDeposit( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, @@ -299,7 +402,10 @@ class TransactionDeposit( @Transient override val amountType = AmountType.Negative override fun getTitle(context: Context): String { - return context.getString(R.string.transaction_deposit) + val uri = Uri.parse(targetPaytoUri) + return uri.getQueryParameter("receiver-name")?.let { receiverName -> + context.getString(R.string.transaction_deposit_to, receiverName) + } ?: context.getString(R.string.transaction_deposit) } override val generalTitleRes = R.string.transaction_deposit @@ -319,7 +425,8 @@ data class PeerInfoShort( class TransactionPeerPullDebit( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, val exchangeBaseUrl: String, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, @@ -346,7 +453,8 @@ class TransactionPeerPullDebit( class TransactionPeerPullCredit( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, val exchangeBaseUrl: String, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, @@ -374,13 +482,14 @@ class TransactionPeerPullCredit( class TransactionPeerPushDebit( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, val exchangeBaseUrl: String, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, override val amountEffective: Amount, val info: PeerInfoShort, - val talerUri: String, + val talerUri: String? = null, // val completed: Boolean, definitely ) : Transaction() { override val icon = R.drawable.ic_cash_usd_outline @@ -403,7 +512,8 @@ class TransactionPeerPushDebit( class TransactionPeerPushCredit( override val transactionId: String, override val timestamp: Timestamp, - override val extendedStatus: ExtendedStatus, + override val txState: TransactionState, + override val txActions: List<TransactionAction>, val exchangeBaseUrl: String, override val error: TalerErrorInfo? = null, override val amountRaw: Amount, @@ -421,3 +531,65 @@ class TransactionPeerPushCredit( 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<TransactionAction>, + 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<TransactionAction> = 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) + } +} |