summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt
diff options
context:
space:
mode:
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.kt457
1 files changed, 398 insertions, 59 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 50181c5..7ccdbde 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,92 @@
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.lib.common.Amount
-import net.taler.lib.common.Timestamp
+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 pending: Boolean
+ abstract val txState: TransactionState
+ abstract val txActions: List<TransactionAction>
abstract val error: TalerErrorInfo?
abstract val amountRaw: Amount
abstract val amountEffective: Amount
@@ -59,6 +121,28 @@ sealed class Transaction {
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()
@@ -70,12 +154,14 @@ sealed class AmountType {
class TransactionWithdrawal(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
+ val kycUrl: String? = null,
val exchangeBaseUrl: String,
val withdrawalDetails: WithdrawalDetails,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_withdrawal
@@ -86,7 +172,7 @@ class TransactionWithdrawal(
override fun getTitle(context: Context) = cleanExchange(exchangeBaseUrl)
override val generalTitleRes = R.string.withdraw_title
val confirmed: Boolean
- get() = !pending && (
+ get() = txState.major != Pending && (
(withdrawalDetails is TalerBankIntegrationApi && withdrawalDetails.confirmed) ||
withdrawalDetails is ManualTransfer
)
@@ -97,12 +183,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
@@ -124,16 +205,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 pending: Boolean,
+ 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
+ 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
@@ -151,7 +324,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.
@@ -164,35 +337,17 @@ 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 pending: Boolean,
+ 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
+ override val amountEffective: Amount,
) : Transaction() {
override val icon = R.drawable.transaction_refund
override val detailPageNav = R.id.action_nav_transactions_detail_refund
@@ -200,56 +355,240 @@ 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)
+ val merchantName = paymentInfo?.merchant?.name ?: "null"
+ return context.getString(R.string.transaction_refund_from, merchantName)
}
override val generalTitleRes = R.string.refund_title
}
@Serializable
-@SerialName("tip")
-class TransactionTip(
+@SerialName("refresh")
+class TransactionRefresh(
+ 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,
+) : 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<TransactionAction>,
+ 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 pending: Boolean,
- // TODO status: TipStatus,
+ override val txState: TransactionState,
+ override val txActions: List<TransactionAction>,
val exchangeBaseUrl: String,
- val merchant: ContractMerchant,
override val error: TalerErrorInfo? = null,
override val amountRaw: Amount,
- override val amountEffective: Amount
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
) : Transaction() {
- override val icon = R.drawable.transaction_tip_accepted // TODO different when declined
- override val detailPageNav = 0
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val detailPageNav = R.id.nav_transactions_detail_peer
@Transient
- override val amountType = AmountType.Positive
+ override val amountType = AmountType.Negative
override fun getTitle(context: Context): String {
- return context.getString(R.string.transaction_tip_from, merchant.name)
+ return context.getString(R.string.transaction_peer_pull_debit)
}
- override val generalTitleRes = R.string.tip_title
+ override val generalTitleRes = R.string.transaction_peer_pull_debit
}
+/**
+ * Credit because someone paid for an invoice we created.
+ */
@Serializable
-@SerialName("refresh")
-class TransactionRefresh(
+@SerialName("peer-pull-credit")
+class TransactionPeerPullCredit(
override val transactionId: String,
override val timestamp: Timestamp,
- override val pending: Boolean,
+ 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
+ override val amountEffective: Amount,
+ val info: PeerInfoShort,
+ val talerUri: String,
+ // val completed: Boolean, maybe
) : Transaction() {
- override val icon = R.drawable.transaction_refresh
- override val detailPageNav = R.id.action_nav_transactions_detail_refresh
+ 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<TransactionAction>,
+ 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_refresh)
+ return context.getString(R.string.transaction_peer_push_debit)
}
- override val generalTitleRes = R.string.transaction_refresh
+ 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<TransactionAction>,
+ 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<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)
+ }
}