From e74f39ee86f32b4e0324405af1f0c7be061fb372 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 12 May 2020 15:26:44 -0300 Subject: [wallet] separate history and transactions UI The history with its JSON payload is only shown in dev mode while the transactions are prepared to move to the new API. --- .../src/main/java/net/taler/wallet/MainActivity.kt | 5 +- .../main/java/net/taler/wallet/MainViewModel.kt | 19 +- .../net/taler/wallet/history/DevHistoryAdapter.kt | 133 ++++++ .../net/taler/wallet/history/DevHistoryFragment.kt | 87 ++++ .../net/taler/wallet/history/DevHistoryManager.kt | 78 ++++ .../java/net/taler/wallet/history/HistoryEvent.kt | 518 +++++++++++++++++++++ .../net/taler/wallet/history/JsonDialogFragment.kt | 57 +++ .../net/taler/wallet/history/ReserveTransaction.kt | 59 +++ .../wallet/transactions/JsonDialogFragment.kt | 57 --- .../wallet/transactions/ReserveTransaction.kt | 59 --- .../net/taler/wallet/transactions/Transaction.kt | 497 -------------------- .../wallet/transactions/TransactionAdapter.kt | 38 +- .../transactions/TransactionDetailFragment.kt | 17 +- .../wallet/transactions/TransactionManager.kt | 72 ++- .../wallet/transactions/TransactionsFragment.kt | 16 +- wallet/src/main/res/drawable/ic_history.xml | 9 + .../src/main/res/layout/fragment_transactions.xml | 2 +- wallet/src/main/res/layout/list_item_history.xml | 75 +++ .../src/main/res/layout/list_item_transaction.xml | 75 --- wallet/src/main/res/menu/activity_main_drawer.xml | 27 +- wallet/src/main/res/navigation/nav_graph.xml | 10 + wallet/src/main/res/values/strings.xml | 1 + .../net/taler/wallet/history/HistoryEventTest.kt | 488 +++++++++++++++++++ .../wallet/history/ReserveHistoryEventTest.kt | 53 +++ .../wallet/transactions/ReserveTransactionTest.kt | 52 --- .../taler/wallet/transactions/TransactionTest.kt | 470 ------------------- 26 files changed, 1680 insertions(+), 1294 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt create mode 100644 wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt create mode 100644 wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt create mode 100644 wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt create mode 100644 wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt create mode 100644 wallet/src/main/res/drawable/ic_history.xml create mode 100644 wallet/src/main/res/layout/list_item_history.xml delete mode 100644 wallet/src/main/res/layout/list_item_transaction.xml create mode 100644 wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt create mode 100644 wallet/src/test/java/net/taler/wallet/history/ReserveHistoryEventTest.kt delete mode 100644 wallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt delete mode 100644 wallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index b6e9a7a..a6385a9 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -75,7 +75,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { setSupportActionBar(toolbar) val appBarConfiguration = AppBarConfiguration( - setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations), + setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations, R.id.nav_history), drawer_layout ) toolbar.setupWithNavController(nav, appBarConfiguration) @@ -86,7 +86,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { val versionView: TextView = nav_view.getHeaderView(0).findViewById(R.id.versionView) model.devMode.observe(this, Observer { enabled -> - nav_view.menu.findItem(R.id.nav_pending_operations).isVisible = enabled + nav_view.menu.findItem(R.id.nav_dev).isVisible = enabled if (enabled) { @SuppressLint("SetTextI18n") versionView.text = "$VERSION_NAME ($VERSION_CODE)" @@ -116,6 +116,7 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { R.id.nav_home -> nav.navigate(R.id.nav_main) R.id.nav_settings -> nav.navigate(R.id.nav_settings) R.id.nav_pending_operations -> nav.navigate(R.id.nav_pending_operations) + R.id.nav_history -> nav.navigate(R.id.nav_history) } drawer_layout.closeDrawer(START) return true diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 230c310..b880036 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -23,11 +23,13 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.viewModelScope import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import net.taler.common.Amount import net.taler.wallet.backend.WalletBackendApi +import net.taler.wallet.history.DevHistoryManager import net.taler.wallet.payment.PaymentManager import net.taler.wallet.pending.PendingOperationsManager import net.taler.wallet.refund.RefundManager @@ -70,13 +72,13 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { loadBalances() if (payload.optString("type") in transactionNotifications) { // update transaction list - // TODO do this in a better way - transactionManager.showAll.value?.let { - transactionManager.showAll.postValue(it) - } + transactionManager.loadTransactions() + } + // refresh pending ops and history with each notification + if (devMode.value == true) { + pendingOperationsManager.getPending() + historyManager.loadHistory() } - // refresh pending ops with each notification - if (devMode.value == true) pendingOperationsManager.getPending() } } @@ -88,7 +90,10 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) { val paymentManager = PaymentManager(walletBackendApi, mapper) val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(walletBackendApi) - val transactionManager: TransactionManager = TransactionManager(walletBackendApi, mapper) + val historyManager: DevHistoryManager = + DevHistoryManager(walletBackendApi, viewModelScope, mapper) + val transactionManager: TransactionManager = + TransactionManager(walletBackendApi, viewModelScope, mapper) val refundManager = RefundManager(walletBackendApi) override fun onCleared() { diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt new file mode 100644 index 0000000..88db90c --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/history/DevHistoryAdapter.kt @@ -0,0 +1,133 @@ +/* + * 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.history + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import net.taler.common.exhaustive +import net.taler.common.toRelativeTime +import net.taler.wallet.R +import net.taler.wallet.history.DevHistoryAdapter.HistoryViewHolder + +@Deprecated("Replaced by TransactionAdapter") +internal class DevHistoryAdapter( + private val listener: OnEventClickListener, + private var history: History = History() +) : Adapter() { + + init { + setHasStableIds(false) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_history, parent, false) + return HistoryViewHolder(view) + } + + override fun getItemCount(): Int = history.size + + override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { + val transaction = history[position] + holder.bind(transaction) + } + + fun update(updatedHistory: History) { + this.history = updatedHistory + this.notifyDataSetChanged() + } + + internal open inner class HistoryViewHolder(private val v: View) : ViewHolder(v) { + + protected val context: Context = v.context + + private val icon: ImageView = v.findViewById(R.id.icon) + protected val title: TextView = v.findViewById(R.id.title) + private val time: TextView = v.findViewById(R.id.time) + private val amount: TextView = v.findViewById(R.id.amount) + + private val amountColor = amount.currentTextColor + + open fun bind(historyEvent: HistoryEvent) { + v.setOnClickListener { listener.onTransactionClicked(historyEvent) } + icon.setImageResource(historyEvent.icon) + + title.text = if (historyEvent.title == null) { + when (historyEvent) { + is RefreshHistoryEvent -> getRefreshTitle(historyEvent) + is OrderAcceptedHistoryEvent -> context.getString(R.string.transaction_order_accepted) + is OrderRefusedHistoryEvent -> context.getString(R.string.transaction_order_refused) + is TipAcceptedHistoryEvent -> context.getString(R.string.transaction_tip_accepted) + is TipDeclinedHistoryEvent -> context.getString(R.string.transaction_tip_declined) + is ReserveBalanceUpdatedHistoryEvent -> context.getString(R.string.transaction_reserve_balance_updated) + else -> historyEvent::class.java.simpleName + } + } else historyEvent.title + + time.text = historyEvent.timestamp.ms.toRelativeTime(context) + bindAmount(historyEvent.displayAmount) + } + + private fun bindAmount(displayAmount: DisplayAmount?) { + if (displayAmount == null) { + amount.visibility = GONE + } else { + amount.visibility = VISIBLE + when (displayAmount.type) { + AmountType.Positive -> { + amount.text = context.getString( + R.string.amount_positive, displayAmount.amount.amountStr + ) + amount.setTextColor(context.getColor(R.color.green)) + } + AmountType.Negative -> { + amount.text = context.getString( + R.string.amount_negative, displayAmount.amount.amountStr + ) + amount.setTextColor(context.getColor(R.color.red)) + } + AmountType.Neutral -> { + amount.text = displayAmount.amount.amountStr + amount.setTextColor(amountColor) + } + }.exhaustive + } + } + + private fun getRefreshTitle(transaction: RefreshHistoryEvent): String { + val res = when (transaction.refreshReason) { + RefreshReason.MANUAL -> R.string.transaction_refresh_reason_manual + RefreshReason.PAY -> R.string.transaction_refresh_reason_pay + RefreshReason.REFUND -> R.string.transaction_refresh_reason_refund + RefreshReason.ABORT_PAY -> R.string.transaction_refresh_reason_abort_pay + RefreshReason.RECOUP -> R.string.transaction_refresh_reason_recoup + RefreshReason.BACKUP_RESTORED -> R.string.transaction_refresh_reason_backup_restored + } + return context.getString(R.string.transaction_refresh) + " " + context.getString(res) + } + + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt new file mode 100644 index 0000000..c3c07a3 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/history/DevHistoryFragment.kt @@ -0,0 +1,87 @@ +/* + * 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.history + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL +import kotlinx.android.synthetic.main.fragment_transactions.* +import net.taler.common.fadeIn +import net.taler.common.fadeOut +import net.taler.wallet.MainViewModel +import net.taler.wallet.R + +internal interface OnEventClickListener { + fun onTransactionClicked(historyEvent: HistoryEvent) +} + +class DevHistoryFragment : Fragment(), + OnEventClickListener { + + private val model: MainViewModel by activityViewModels() + private val historyManager by lazy { model.historyManager } + private val historyAdapter by lazy { DevHistoryAdapter(this) } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_transactions, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (savedInstanceState == null) historyManager.loadHistory() + + list.apply { + adapter = historyAdapter + addItemDecoration(DividerItemDecoration(context, VERTICAL)) + } + historyManager.progress.observe(viewLifecycleOwner, Observer { show -> + progressBar.visibility = if (show) VISIBLE else INVISIBLE + }) + historyManager.history.observe(viewLifecycleOwner, Observer { result -> + onHistoryResult(result) + }) + } + + override fun onTransactionClicked(historyEvent: HistoryEvent) { + JsonDialogFragment.new(historyEvent.json.toString(2)) + .show(parentFragmentManager, null) + } + + private fun onHistoryResult(result: HistoryResult) = when (result) { + HistoryResult.Error -> { + list.fadeOut() + emptyState.text = getString(R.string.transactions_error) + emptyState.fadeIn() + } + is HistoryResult.Success -> { + emptyState.visibility = if (result.history.isEmpty()) VISIBLE else INVISIBLE + historyAdapter.update(result.history) + list.fadeIn() + } + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt b/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt new file mode 100644 index 0000000..72967b2 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/history/DevHistoryManager.kt @@ -0,0 +1,78 @@ +/* + * 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.history + +import androidx.annotation.UiThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.taler.wallet.backend.WalletBackendApi +import org.json.JSONObject + +sealed class HistoryResult { + object Error : HistoryResult() + class Success(val history: History) : HistoryResult() +} + +class DevHistoryManager( + private val walletBackendApi: WalletBackendApi, + private val scope: CoroutineScope, + private val mapper: ObjectMapper +) { + + private val mProgress = MutableLiveData() + val progress: LiveData = mProgress + + private val mHistory = MutableLiveData() + val history: LiveData = mHistory + + @UiThread + internal fun loadHistory() { + mProgress.value = true + walletBackendApi.sendRequest("getHistory", null) { isError, result -> + scope.launch(Dispatchers.Default) { + onEventsLoaded(isError, result) + } + } + } + + private fun onEventsLoaded(isError: Boolean, result: JSONObject) { + if (isError) { + mHistory.postValue(HistoryResult.Error) + return + } + val history = History() + val json = result.getJSONArray("history") + for (i in 0 until json.length()) { + val event: HistoryEvent = mapper.readValue(json.getString(i)) + event.json = json.getJSONObject(i) + history.add(event) + } + history.reverse() // show latest first + mProgress.postValue(false) + mHistory.postValue( + HistoryResult.Success( + history + ) + ) + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt new file mode 100644 index 0000000..acca679 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt @@ -0,0 +1,518 @@ +/* + * 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.history + +import androidx.annotation.DrawableRes +import androidx.annotation.LayoutRes +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonSubTypes.Type +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME +import com.fasterxml.jackson.annotation.JsonTypeName +import net.taler.common.Amount +import net.taler.common.Timestamp +import net.taler.wallet.R +import net.taler.wallet.cleanExchange +import org.json.JSONObject + +enum class ReserveType { + /** + * Manually created. + */ + @JsonProperty("manual") + MANUAL, + + /** + * Withdrawn from a bank that has "tight" Taler integration + */ + @JsonProperty("taler-bank-withdraw") + @Suppress("unused") + TALER_BANK_WITHDRAW, +} + +@JsonInclude(NON_EMPTY) +class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?) + +enum class RefreshReason { + @JsonProperty("manual") + @Suppress("unused") + MANUAL, + + @JsonProperty("pay") + PAY, + + @JsonProperty("refund") + @Suppress("unused") + REFUND, + + @JsonProperty("abort-pay") + @Suppress("unused") + ABORT_PAY, + + @JsonProperty("recoup") + @Suppress("unused") + RECOUP, + + @JsonProperty("backup-restored") + @Suppress("unused") + BACKUP_RESTORED +} + +@JsonInclude(NON_EMPTY) +class ReserveShortInfo( + /** + * The exchange that the reserve will be at. + */ + val exchangeBaseUrl: String, + /** + * Key to query more details + */ + val reservePub: String, + /** + * Detail about how the reserve has been created. + */ + val reserveCreationDetail: ReserveCreationDetail +) + +sealed class AmountType { + object Positive : AmountType() + object Negative : AmountType() + object Neutral : AmountType() +} + +class DisplayAmount( + val amount: Amount, + val type: AmountType +) + +typealias History = ArrayList + +@JsonTypeInfo( + use = NAME, + include = PROPERTY, + property = "type", + defaultImpl = UnknownHistoryEvent::class +) +/** missing: +AuditorComplaintSent = "auditor-complained-sent", +AuditorComplaintProcessed = "auditor-complaint-processed", +AuditorTrustAdded = "auditor-trust-added", +AuditorTrustRemoved = "auditor-trust-removed", +ExchangeTermsAccepted = "exchange-terms-accepted", +ExchangePolicyChanged = "exchange-policy-changed", +ExchangeTrustAdded = "exchange-trust-added", +ExchangeTrustRemoved = "exchange-trust-removed", +FundsDepositedToSelf = "funds-deposited-to-self", +FundsRecouped = "funds-recouped", +ReserveCreated = "reserve-created", + */ +@JsonSubTypes( + Type(value = ExchangeAddedEvent::class, name = "exchange-added"), + Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"), + Type(value = ReserveBalanceUpdatedHistoryEvent::class, name = "reserve-balance-updated"), + Type(value = WithdrawHistoryEvent::class, name = "withdrawn"), + Type(value = OrderAcceptedHistoryEvent::class, name = "order-accepted"), + Type(value = OrderRefusedHistoryEvent::class, name = "order-refused"), + Type(value = OrderRedirectedHistoryEvent::class, name = "order-redirected"), + Type(value = PaymentHistoryEvent::class, name = "payment-sent"), + Type(value = PaymentAbortedHistoryEvent::class, name = "payment-aborted"), + Type(value = TipAcceptedHistoryEvent::class, name = "tip-accepted"), + Type(value = TipDeclinedHistoryEvent::class, name = "tip-declined"), + Type(value = RefundHistoryEvent::class, name = "refund"), + Type(value = RefreshHistoryEvent::class, name = "refreshed") +) +abstract class HistoryEvent( + val timestamp: Timestamp, + val eventId: String, + @get:LayoutRes + open val detailPageLayout: Int = 0, + @get:DrawableRes + open val icon: Int = R.drawable.ic_account_balance, + open val showToUser: Boolean = false +) { + abstract val title: String? + open lateinit var json: JSONObject + open val displayAmount: DisplayAmount? = null + open fun isCurrency(currency: String): Boolean = true +} + + +class UnknownHistoryEvent(timestamp: Timestamp, eventId: String) : HistoryEvent(timestamp, eventId) { + override val title: String? = null +} + +@JsonTypeName("exchange-added") +class ExchangeAddedEvent( + timestamp: Timestamp, + eventId: String, + val exchangeBaseUrl: String, + val builtIn: Boolean +) : HistoryEvent(timestamp, eventId) { + override val title = cleanExchange(exchangeBaseUrl) +} + +@JsonTypeName("exchange-updated") +class ExchangeUpdatedEvent( + timestamp: Timestamp, + eventId: String, + val exchangeBaseUrl: String +) : HistoryEvent(timestamp, eventId) { + override val title = cleanExchange(exchangeBaseUrl) +} + + +@JsonTypeName("reserve-balance-updated") +class ReserveBalanceUpdatedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Condensed information about the reserve. + */ + val reserveShortInfo: ReserveShortInfo, + /** + * Amount currently left in the reserve. + */ + val reserveBalance: Amount, + /** + * Amount we expected to be in the reserve at that time, + * considering ongoing withdrawals from that reserve. + */ + val reserveAwaitedAmount: Amount, + /** + * Amount that hasn't been withdrawn yet. + */ + val reserveUnclaimedAmount: Amount +) : HistoryEvent(timestamp, eventId) { + override val title: String? = null + override val displayAmount = DisplayAmount( + reserveBalance, + AmountType.Neutral + ) + override fun isCurrency(currency: String) = reserveBalance.currency == currency +} + +@JsonTypeName("withdrawn") +class WithdrawHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Exchange that was withdrawn from. + */ + val exchangeBaseUrl: String, + /** + * Unique identifier for the withdrawal session, can be used to + * query more detailed information from the wallet. + */ + val withdrawalGroupId: String, + val withdrawalSource: WithdrawalSource, + /** + * Amount that has been subtracted from the reserve's balance + * for this withdrawal. + */ + val amountWithdrawnRaw: Amount, + /** + * Amount that actually was added to the wallet's balance. + */ + val amountWithdrawnEffective: Amount +) : HistoryEvent(timestamp, eventId) { + override val detailPageLayout = R.layout.fragment_event_withdraw + override val title = cleanExchange(exchangeBaseUrl) + override val icon = R.drawable.transaction_withdrawal + override val showToUser = true + override val displayAmount = DisplayAmount( + amountWithdrawnEffective, + AmountType.Positive + ) + override fun isCurrency(currency: String) = amountWithdrawnRaw.currency == currency +} + +@JsonTypeName("order-accepted") +class OrderAcceptedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Condensed info about the order. + */ + val orderShortInfo: OrderShortInfo +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.ic_add_circle + override val title: String? = null + override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency +} + +@JsonTypeName("order-refused") +class OrderRefusedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Condensed info about the order. + */ + val orderShortInfo: OrderShortInfo +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.ic_cancel + override val title: String? = null + override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency +} + +@JsonTypeName("payment-sent") +class PaymentHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Condensed info about the order that we already paid for. + */ + val orderShortInfo: OrderShortInfo, + /** + * Set to true if the payment has been previously sent + * to the merchant successfully, possibly with a different session ID. + */ + val replay: Boolean, + /** + * Number of coins that were involved in the payment. + */ + val numCoins: Int, + /** + * Amount that was paid, including deposit and wire fees. + */ + val amountPaidWithFees: Amount, + /** + * Session ID that the payment was (re-)submitted under. + */ + val sessionId: String? +) : HistoryEvent(timestamp, eventId) { + override val detailPageLayout = R.layout.fragment_event_paid + override val title = orderShortInfo.summary + override val icon = R.drawable.ic_cash_usd_outline + override val showToUser = true + override val displayAmount = DisplayAmount( + amountPaidWithFees, + AmountType.Negative + ) + override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency +} + +@JsonTypeName("payment-aborted") +class PaymentAbortedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Condensed info about the order that we already paid for. + */ + val orderShortInfo: OrderShortInfo, + /** + * Amount that was lost due to refund and refreshing fees. + */ + val amountLost: Amount +) : HistoryEvent(timestamp, eventId) { + override val title = orderShortInfo.summary + override val icon = R.drawable.transaction_payment_aborted + override val showToUser = true + override val displayAmount = DisplayAmount( + amountLost, + AmountType.Negative + ) + override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency +} + +@JsonTypeName("refreshed") +class RefreshHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Amount that is now available again because it has + * been refreshed. + */ + val amountRefreshedEffective: Amount, + /** + * Amount that we spent for refreshing. + */ + val amountRefreshedRaw: Amount, + /** + * Why was the refreshing done? + */ + val refreshReason: RefreshReason, + val numInputCoins: Int, + val numRefreshedInputCoins: Int, + val numOutputCoins: Int, + /** + * Identifier for a refresh group, contains one or + * more refresh session IDs. + */ + val refreshGroupId: String +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.transaction_refresh + override val title: String? = null + override val showToUser = !(amountRefreshedRaw - amountRefreshedEffective).isZero() + override val displayAmount: DisplayAmount? + get() { + return if (showToUser) DisplayAmount( + amountRefreshedRaw - amountRefreshedEffective, + AmountType.Negative + ) + else null + } + + override fun isCurrency(currency: String) = amountRefreshedRaw.currency == currency +} + +@JsonTypeName("order-redirected") +class OrderRedirectedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Condensed info about the new order that contains a + * product (identified by the fulfillment URL) that we've already paid for. + */ + val newOrderShortInfo: OrderShortInfo, + /** + * Condensed info about the order that we already paid for. + */ + val alreadyPaidOrderShortInfo: OrderShortInfo +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.ic_directions + override val title = newOrderShortInfo.summary + override fun isCurrency(currency: String) = newOrderShortInfo.amount.currency == currency +} + +@JsonTypeName("tip-accepted") +class TipAcceptedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Unique identifier for the tip to query more information. + */ + val tipId: String, + /** + * Raw amount of the tip, without extra fees that apply. + */ + val tipRaw: Amount +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.transaction_tip_accepted + override val title: String? = null + override val showToUser = true + override val displayAmount = DisplayAmount( + tipRaw, + AmountType.Positive + ) + override fun isCurrency(currency: String) = tipRaw.currency == currency +} + +@JsonTypeName("tip-declined") +class TipDeclinedHistoryEvent( + timestamp: Timestamp, + eventId: String, + /** + * Unique identifier for the tip to query more information. + */ + val tipId: String, + /** + * Raw amount of the tip, without extra fees that apply. + */ + val tipAmount: Amount +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.transaction_tip_declined + override val title: String? = null + override val showToUser = true + override val displayAmount = DisplayAmount( + tipAmount, + AmountType.Neutral + ) + override fun isCurrency(currency: String) = tipAmount.currency == currency +} + +@JsonTypeName("refund") +class RefundHistoryEvent( + timestamp: Timestamp, + eventId: String, + val orderShortInfo: OrderShortInfo, + /** + * Unique identifier for this refund. + * (Identifies multiple refund permissions that were obtained at once.) + */ + val refundGroupId: String, + /** + * Part of the refund that couldn't be applied because + * the refund permissions were expired. + */ + val amountRefundedInvalid: Amount, + /** + * Amount that has been refunded by the merchant. + */ + val amountRefundedRaw: Amount, + /** + * Amount will be added to the wallet's balance after fees and refreshing. + */ + val amountRefundedEffective: Amount +) : HistoryEvent(timestamp, eventId) { + override val icon = R.drawable.transaction_refund + override val title = orderShortInfo.summary + override val detailPageLayout = R.layout.fragment_event_paid + override val showToUser = true + override val displayAmount = DisplayAmount( + amountRefundedEffective, + AmountType.Positive + ) + override fun isCurrency(currency: String) = amountRefundedRaw.currency == currency +} + +@JsonTypeInfo( + use = NAME, + include = PROPERTY, + property = "type" +) +@JsonSubTypes( + Type(value = WithdrawalSourceReserve::class, name = "reserve") +) +abstract class WithdrawalSource + +@Suppress("unused") +@JsonTypeName("tip") +class WithdrawalSourceTip( + val tipId: String +) : WithdrawalSource() + +@JsonTypeName("reserve") +class WithdrawalSourceReserve( + val reservePub: String +) : WithdrawalSource() + +data class OrderShortInfo( + /** + * Wallet-internal identifier of the proposal. + */ + val proposalId: String, + /** + * Order ID, uniquely identifies the order within a merchant instance. + */ + val orderId: String, + /** + * Base URL of the merchant. + */ + val merchantBaseUrl: String, + /** + * Amount that must be paid for the contract. + */ + val amount: Amount, + /** + * Summary of the proposal, given by the merchant. + */ + val summary: String +) diff --git a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt new file mode 100644 index 0000000..31c2b93 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt @@ -0,0 +1,57 @@ +/* + * 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.history + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.fragment.app.DialogFragment +import kotlinx.android.synthetic.main.fragment_json.* +import net.taler.wallet.R + +class JsonDialogFragment : DialogFragment() { + + companion object { + fun new(json: String): JsonDialogFragment { + return JsonDialogFragment().apply { + arguments = Bundle().apply { putString("json", json) } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_json, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val json = requireArguments().getString("json") + jsonView.text = json + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout(MATCH_PARENT, WRAP_CONTENT) + } + +} diff --git a/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt new file mode 100644 index 0000000..6c8fdaa --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt @@ -0,0 +1,59 @@ +/* + * 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.history + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME +import com.fasterxml.jackson.annotation.JsonTypeName +import net.taler.common.Timestamp + + +@JsonTypeInfo( + use = NAME, + include = PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = ReserveDepositTransaction::class, name = "DEPOSIT") +) +abstract class ReserveTransaction + + +@JsonTypeName("DEPOSIT") +class ReserveDepositTransaction( + /** + * Amount withdrawn. + */ + val amount: String, + /** + * Sender account payto://-URL + */ + @JsonProperty("sender_account_url") + val senderAccountUrl: String, + /** + * Transfer details uniquely identifying the transfer. + */ + @JsonProperty("wire_reference") + val wireReference: String, + /** + * Timestamp of the incoming wire transfer. + */ + val timestamp: Timestamp +) : ReserveTransaction() diff --git a/wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt deleted file mode 100644 index 2337059..0000000 --- a/wallet/src/main/java/net/taler/wallet/transactions/JsonDialogFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import androidx.fragment.app.DialogFragment -import kotlinx.android.synthetic.main.fragment_json.* -import net.taler.wallet.R - -class JsonDialogFragment : DialogFragment() { - - companion object { - fun new(json: String): JsonDialogFragment { - return JsonDialogFragment().apply { - arguments = Bundle().apply { putString("json", json) } - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_json, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val json = requireArguments().getString("json") - jsonView.text = json - } - - override fun onStart() { - super.onStart() - dialog?.window?.setLayout(MATCH_PARENT, WRAP_CONTENT) - } - -} diff --git a/wallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt b/wallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt deleted file mode 100644 index e497e9a..0000000 --- a/wallet/src/main/java/net/taler/wallet/transactions/ReserveTransaction.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY -import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME -import com.fasterxml.jackson.annotation.JsonTypeName -import net.taler.common.Timestamp - - -@JsonTypeInfo( - use = NAME, - include = PROPERTY, - property = "type" -) -@JsonSubTypes( - JsonSubTypes.Type(value = ReserveDepositTransaction::class, name = "DEPOSIT") -) -abstract class ReserveTransaction - - -@JsonTypeName("DEPOSIT") -class ReserveDepositTransaction( - /** - * Amount withdrawn. - */ - val amount: String, - /** - * Sender account payto://-URL - */ - @JsonProperty("sender_account_url") - val senderAccountUrl: String, - /** - * Transfer details uniquely identifying the transfer. - */ - @JsonProperty("wire_reference") - val wireReference: String, - /** - * Timestamp of the incoming wire transfer. - */ - val timestamp: Timestamp -) : ReserveTransaction() diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt deleted file mode 100644 index 34942d0..0000000 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transaction.kt +++ /dev/null @@ -1,497 +0,0 @@ -/* - * 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 androidx.annotation.DrawableRes -import androidx.annotation.LayoutRes -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonSubTypes.Type -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY -import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME -import com.fasterxml.jackson.annotation.JsonTypeName -import net.taler.common.Amount -import net.taler.common.Timestamp -import net.taler.wallet.R -import net.taler.wallet.cleanExchange -import org.json.JSONObject - -enum class ReserveType { - /** - * Manually created. - */ - @JsonProperty("manual") - MANUAL, - - /** - * Withdrawn from a bank that has "tight" Taler integration - */ - @JsonProperty("taler-bank-withdraw") - @Suppress("unused") - TALER_BANK_WITHDRAW, -} - -@JsonInclude(NON_EMPTY) -class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?) - -enum class RefreshReason { - @JsonProperty("manual") - @Suppress("unused") - MANUAL, - - @JsonProperty("pay") - PAY, - - @JsonProperty("refund") - @Suppress("unused") - REFUND, - - @JsonProperty("abort-pay") - @Suppress("unused") - ABORT_PAY, - - @JsonProperty("recoup") - @Suppress("unused") - RECOUP, - - @JsonProperty("backup-restored") - @Suppress("unused") - BACKUP_RESTORED -} - -@JsonInclude(NON_EMPTY) -class ReserveShortInfo( - /** - * The exchange that the reserve will be at. - */ - val exchangeBaseUrl: String, - /** - * Key to query more details - */ - val reservePub: String, - /** - * Detail about how the reserve has been created. - */ - val reserveCreationDetail: ReserveCreationDetail -) - -sealed class AmountType { - object Positive : AmountType() - object Negative : AmountType() - object Neutral : AmountType() -} - -class DisplayAmount( - val amount: Amount, - val type: AmountType -) - -typealias Transactions = ArrayList - -@JsonTypeInfo( - use = NAME, - include = PROPERTY, - property = "type", - defaultImpl = UnknownTransaction::class -) -/** missing: -AuditorComplaintSent = "auditor-complained-sent", -AuditorComplaintProcessed = "auditor-complaint-processed", -AuditorTrustAdded = "auditor-trust-added", -AuditorTrustRemoved = "auditor-trust-removed", -ExchangeTermsAccepted = "exchange-terms-accepted", -ExchangePolicyChanged = "exchange-policy-changed", -ExchangeTrustAdded = "exchange-trust-added", -ExchangeTrustRemoved = "exchange-trust-removed", -FundsDepositedToSelf = "funds-deposited-to-self", -FundsRecouped = "funds-recouped", -ReserveCreated = "reserve-created", - */ -@JsonSubTypes( - Type(value = ExchangeAddedEvent::class, name = "exchange-added"), - Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"), - Type(value = ReserveBalanceUpdatedTransaction::class, name = "reserve-balance-updated"), - Type(value = WithdrawTransaction::class, name = "withdrawn"), - Type(value = OrderAcceptedTransaction::class, name = "order-accepted"), - Type(value = OrderRefusedTransaction::class, name = "order-refused"), - Type(value = OrderRedirectedTransaction::class, name = "order-redirected"), - Type(value = PaymentTransaction::class, name = "payment-sent"), - Type(value = PaymentAbortedTransaction::class, name = "payment-aborted"), - Type(value = TipAcceptedTransaction::class, name = "tip-accepted"), - Type(value = TipDeclinedTransaction::class, name = "tip-declined"), - Type(value = RefundTransaction::class, name = "refund"), - Type(value = RefreshTransaction::class, name = "refreshed") -) -abstract class Transaction( - val timestamp: Timestamp, - val eventId: String, - @get:LayoutRes - open val detailPageLayout: Int = 0, - @get:DrawableRes - open val icon: Int = R.drawable.ic_account_balance, - open val showToUser: Boolean = false -) { - abstract val title: String? - open lateinit var json: JSONObject - open val displayAmount: DisplayAmount? = null - open fun isCurrency(currency: String): Boolean = true -} - - -class UnknownTransaction(timestamp: Timestamp, eventId: String) : Transaction(timestamp, eventId) { - override val title: String? = null -} - -@JsonTypeName("exchange-added") -class ExchangeAddedEvent( - timestamp: Timestamp, - eventId: String, - val exchangeBaseUrl: String, - val builtIn: Boolean -) : Transaction(timestamp, eventId) { - override val title = cleanExchange(exchangeBaseUrl) -} - -@JsonTypeName("exchange-updated") -class ExchangeUpdatedEvent( - timestamp: Timestamp, - eventId: String, - val exchangeBaseUrl: String -) : Transaction(timestamp, eventId) { - override val title = cleanExchange(exchangeBaseUrl) -} - - -@JsonTypeName("reserve-balance-updated") -class ReserveBalanceUpdatedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Condensed information about the reserve. - */ - val reserveShortInfo: ReserveShortInfo, - /** - * Amount currently left in the reserve. - */ - val reserveBalance: Amount, - /** - * Amount we expected to be in the reserve at that time, - * considering ongoing withdrawals from that reserve. - */ - val reserveAwaitedAmount: Amount, - /** - * Amount that hasn't been withdrawn yet. - */ - val reserveUnclaimedAmount: Amount -) : Transaction(timestamp, eventId) { - override val title: String? = null - override val displayAmount = DisplayAmount(reserveBalance, AmountType.Neutral) - override fun isCurrency(currency: String) = reserveBalance.currency == currency -} - -@JsonTypeName("withdrawn") -class WithdrawTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Exchange that was withdrawn from. - */ - val exchangeBaseUrl: String, - /** - * Unique identifier for the withdrawal session, can be used to - * query more detailed information from the wallet. - */ - val withdrawalGroupId: String, - val withdrawalSource: WithdrawalSource, - /** - * Amount that has been subtracted from the reserve's balance - * for this withdrawal. - */ - val amountWithdrawnRaw: Amount, - /** - * Amount that actually was added to the wallet's balance. - */ - val amountWithdrawnEffective: Amount -) : Transaction(timestamp, eventId) { - override val detailPageLayout = R.layout.fragment_event_withdraw - override val title = cleanExchange(exchangeBaseUrl) - override val icon = R.drawable.transaction_withdrawal - override val showToUser = true - override val displayAmount = DisplayAmount(amountWithdrawnEffective, AmountType.Positive) - override fun isCurrency(currency: String) = amountWithdrawnRaw.currency == currency -} - -@JsonTypeName("order-accepted") -class OrderAcceptedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Condensed info about the order. - */ - val orderShortInfo: OrderShortInfo -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.ic_add_circle - override val title: String? = null - override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency -} - -@JsonTypeName("order-refused") -class OrderRefusedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Condensed info about the order. - */ - val orderShortInfo: OrderShortInfo -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.ic_cancel - override val title: String? = null - override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency -} - -@JsonTypeName("payment-sent") -class PaymentTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Condensed info about the order that we already paid for. - */ - val orderShortInfo: OrderShortInfo, - /** - * Set to true if the payment has been previously sent - * to the merchant successfully, possibly with a different session ID. - */ - val replay: Boolean, - /** - * Number of coins that were involved in the payment. - */ - val numCoins: Int, - /** - * Amount that was paid, including deposit and wire fees. - */ - val amountPaidWithFees: Amount, - /** - * Session ID that the payment was (re-)submitted under. - */ - val sessionId: String? -) : Transaction(timestamp, eventId) { - override val detailPageLayout = R.layout.fragment_event_paid - override val title = orderShortInfo.summary - override val icon = R.drawable.ic_cash_usd_outline - override val showToUser = true - override val displayAmount = DisplayAmount(amountPaidWithFees, AmountType.Negative) - override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency -} - -@JsonTypeName("payment-aborted") -class PaymentAbortedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Condensed info about the order that we already paid for. - */ - val orderShortInfo: OrderShortInfo, - /** - * Amount that was lost due to refund and refreshing fees. - */ - val amountLost: Amount -) : Transaction(timestamp, eventId) { - override val title = orderShortInfo.summary - override val icon = R.drawable.transaction_payment_aborted - override val showToUser = true - override val displayAmount = DisplayAmount(amountLost, AmountType.Negative) - override fun isCurrency(currency: String) = orderShortInfo.amount.currency == currency -} - -@JsonTypeName("refreshed") -class RefreshTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Amount that is now available again because it has - * been refreshed. - */ - val amountRefreshedEffective: Amount, - /** - * Amount that we spent for refreshing. - */ - val amountRefreshedRaw: Amount, - /** - * Why was the refreshing done? - */ - val refreshReason: RefreshReason, - val numInputCoins: Int, - val numRefreshedInputCoins: Int, - val numOutputCoins: Int, - /** - * Identifier for a refresh group, contains one or - * more refresh session IDs. - */ - val refreshGroupId: String -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.transaction_refresh - override val title: String? = null - override val showToUser = !(amountRefreshedRaw - amountRefreshedEffective).isZero() - override val displayAmount: DisplayAmount? - get() { - return if (showToUser) DisplayAmount( - amountRefreshedRaw - amountRefreshedEffective, - AmountType.Negative - ) - else null - } - - override fun isCurrency(currency: String) = amountRefreshedRaw.currency == currency -} - -@JsonTypeName("order-redirected") -class OrderRedirectedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Condensed info about the new order that contains a - * product (identified by the fulfillment URL) that we've already paid for. - */ - val newOrderShortInfo: OrderShortInfo, - /** - * Condensed info about the order that we already paid for. - */ - val alreadyPaidOrderShortInfo: OrderShortInfo -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.ic_directions - override val title = newOrderShortInfo.summary - override fun isCurrency(currency: String) = newOrderShortInfo.amount.currency == currency -} - -@JsonTypeName("tip-accepted") -class TipAcceptedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Unique identifier for the tip to query more information. - */ - val tipId: String, - /** - * Raw amount of the tip, without extra fees that apply. - */ - val tipRaw: Amount -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.transaction_tip_accepted - override val title: String? = null - override val showToUser = true - override val displayAmount = DisplayAmount(tipRaw, AmountType.Positive) - override fun isCurrency(currency: String) = tipRaw.currency == currency -} - -@JsonTypeName("tip-declined") -class TipDeclinedTransaction( - timestamp: Timestamp, - eventId: String, - /** - * Unique identifier for the tip to query more information. - */ - val tipId: String, - /** - * Raw amount of the tip, without extra fees that apply. - */ - val tipAmount: Amount -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.transaction_tip_declined - override val title: String? = null - override val showToUser = true - override val displayAmount = DisplayAmount(tipAmount, AmountType.Neutral) - override fun isCurrency(currency: String) = tipAmount.currency == currency -} - -@JsonTypeName("refund") -class RefundTransaction( - timestamp: Timestamp, - eventId: String, - val orderShortInfo: OrderShortInfo, - /** - * Unique identifier for this refund. - * (Identifies multiple refund permissions that were obtained at once.) - */ - val refundGroupId: String, - /** - * Part of the refund that couldn't be applied because - * the refund permissions were expired. - */ - val amountRefundedInvalid: Amount, - /** - * Amount that has been refunded by the merchant. - */ - val amountRefundedRaw: Amount, - /** - * Amount will be added to the wallet's balance after fees and refreshing. - */ - val amountRefundedEffective: Amount -) : Transaction(timestamp, eventId) { - override val icon = R.drawable.transaction_refund - override val title = orderShortInfo.summary - override val detailPageLayout = R.layout.fragment_event_paid - override val showToUser = true - override val displayAmount = DisplayAmount(amountRefundedEffective, AmountType.Positive) - override fun isCurrency(currency: String) = amountRefundedRaw.currency == currency -} - -@JsonTypeInfo( - use = NAME, - include = PROPERTY, - property = "type" -) -@JsonSubTypes( - Type(value = WithdrawalSourceReserve::class, name = "reserve") -) -abstract class WithdrawalSource - -@Suppress("unused") -@JsonTypeName("tip") -class WithdrawalSourceTip( - val tipId: String -) : WithdrawalSource() - -@JsonTypeName("reserve") -class WithdrawalSourceReserve( - val reservePub: String -) : WithdrawalSource() - -data class OrderShortInfo( - /** - * Wallet-internal identifier of the proposal. - */ - val proposalId: String, - /** - * Order ID, uniquely identifies the order within a merchant instance. - */ - val orderId: String, - /** - * Base URL of the merchant. - */ - val merchantBaseUrl: String, - /** - * Amount that must be paid for the contract. - */ - val amount: Amount, - /** - * Summary of the proposal, given by the merchant. - */ - val summary: String -) diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt index 440d07f..5aca896 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionAdapter.kt @@ -34,13 +34,23 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import net.taler.common.exhaustive import net.taler.common.toRelativeTime import net.taler.wallet.R +import net.taler.wallet.history.AmountType +import net.taler.wallet.history.DisplayAmount +import net.taler.wallet.history.History +import net.taler.wallet.history.HistoryEvent +import net.taler.wallet.history.OrderAcceptedHistoryEvent +import net.taler.wallet.history.OrderRefusedHistoryEvent +import net.taler.wallet.history.RefreshHistoryEvent +import net.taler.wallet.history.RefreshReason +import net.taler.wallet.history.ReserveBalanceUpdatedHistoryEvent +import net.taler.wallet.history.TipAcceptedHistoryEvent +import net.taler.wallet.history.TipDeclinedHistoryEvent import net.taler.wallet.transactions.TransactionAdapter.TransactionViewHolder internal class TransactionAdapter( - private val devMode: Boolean, - private val listener: OnEventClickListener, - private var transactions: Transactions = Transactions() + private val listener: OnTransactionClickListener, + private var transactions: History = History() ) : Adapter() { lateinit var tracker: SelectionTracker @@ -52,7 +62,7 @@ internal class TransactionAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.list_item_transaction, parent, false) + .inflate(R.layout.list_item_history, parent, false) return TransactionViewHolder(view) } @@ -63,7 +73,7 @@ internal class TransactionAdapter( holder.bind(transaction, tracker.isSelected(transaction.eventId)) } - fun update(updatedTransactions: Transactions) { + fun update(updatedTransactions: History) { this.transactions = updatedTransactions this.notifyDataSetChanged() } @@ -84,8 +94,8 @@ internal class TransactionAdapter( private val selectableForeground = v.foreground private val amountColor = amount.currentTextColor - open fun bind(transaction: Transaction, selected: Boolean) { - if (devMode || transaction.detailPageLayout != 0) { + open fun bind(transaction: HistoryEvent, selected: Boolean) { + if (transaction.detailPageLayout != 0) { v.foreground = selectableForeground v.setOnClickListener { listener.onTransactionClicked(transaction) } } else { @@ -97,12 +107,12 @@ internal class TransactionAdapter( title.text = if (transaction.title == null) { when (transaction) { - is RefreshTransaction -> getRefreshTitle(transaction) - is OrderAcceptedTransaction -> context.getString(R.string.transaction_order_accepted) - is OrderRefusedTransaction -> context.getString(R.string.transaction_order_refused) - is TipAcceptedTransaction -> context.getString(R.string.transaction_tip_accepted) - is TipDeclinedTransaction -> context.getString(R.string.transaction_tip_declined) - is ReserveBalanceUpdatedTransaction -> context.getString(R.string.transaction_reserve_balance_updated) + is RefreshHistoryEvent -> getRefreshTitle(transaction) + is OrderAcceptedHistoryEvent -> context.getString(R.string.transaction_order_accepted) + is OrderRefusedHistoryEvent -> context.getString(R.string.transaction_order_refused) + is TipAcceptedHistoryEvent -> context.getString(R.string.transaction_tip_accepted) + is TipDeclinedHistoryEvent -> context.getString(R.string.transaction_tip_declined) + is ReserveBalanceUpdatedHistoryEvent -> context.getString(R.string.transaction_reserve_balance_updated) else -> transaction::class.java.simpleName } } else transaction.title @@ -137,7 +147,7 @@ internal class TransactionAdapter( } } - private fun getRefreshTitle(transaction: RefreshTransaction): String { + private fun getRefreshTitle(transaction: RefreshHistoryEvent): String { val res = when (transaction.refreshReason) { RefreshReason.MANUAL -> R.string.transaction_refresh_reason_manual RefreshReason.PAY -> R.string.transaction_refresh_reason_pay diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt index 909a7bf..bb70b5c 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt @@ -37,6 +37,11 @@ import net.taler.common.toAbsoluteTime import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.cleanExchange +import net.taler.wallet.history.JsonDialogFragment +import net.taler.wallet.history.OrderShortInfo +import net.taler.wallet.history.PaymentHistoryEvent +import net.taler.wallet.history.RefundHistoryEvent +import net.taler.wallet.history.WithdrawHistoryEvent class TransactionDetailFragment : Fragment() { @@ -65,9 +70,9 @@ class TransactionDetailFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { timeView.text = event.timestamp.ms.toAbsoluteTime(requireContext()) when (val e = event) { - is WithdrawTransaction -> bind(e) - is PaymentTransaction -> bind(e) - is RefundTransaction -> bind(e) + is WithdrawHistoryEvent -> bind(e) + is PaymentHistoryEvent -> bind(e) + is RefundHistoryEvent -> bind(e) else -> Toast.makeText( requireContext(), "event ${e.javaClass} not implement", @@ -90,7 +95,7 @@ class TransactionDetailFragment : Fragment() { } } - private fun bind(event: WithdrawTransaction) { + private fun bind(event: WithdrawHistoryEvent) { effectiveAmountLabel.text = getString(R.string.withdraw_total) effectiveAmountView.text = event.amountWithdrawnEffective.toString() chosenAmountLabel.text = getString(R.string.amount_chosen) @@ -101,13 +106,13 @@ class TransactionDetailFragment : Fragment() { exchangeView.text = cleanExchange(event.exchangeBaseUrl) } - private fun bind(event: PaymentTransaction) { + private fun bind(event: PaymentHistoryEvent) { amountPaidWithFeesView.text = event.amountPaidWithFees.toString() val fee = event.amountPaidWithFees - event.orderShortInfo.amount bindOrderAndFee(event.orderShortInfo, fee) } - private fun bind(event: RefundTransaction) { + private fun bind(event: RefundHistoryEvent) { amountPaidWithFeesLabel.text = getString(R.string.transaction_refund) amountPaidWithFeesView.setTextColor(getColor(requireContext(), R.color.green)) amountPaidWithFeesView.text = diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt index 549b2a8..850a3bb 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -18,70 +18,62 @@ package net.taler.wallet.transactions import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asLiveData -import androidx.lifecycle.switchMap import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import net.taler.wallet.backend.WalletBackendApi +import net.taler.wallet.history.History +import net.taler.wallet.history.HistoryEvent +import org.json.JSONObject sealed class TransactionsResult { object Error : TransactionsResult() - class Success(val transactions: Transactions) : TransactionsResult() + class Success(val transactions: History) : TransactionsResult() } -@Suppress("EXPERIMENTAL_API_USAGE") class TransactionManager( private val walletBackendApi: WalletBackendApi, + private val scope: CoroutineScope, private val mapper: ObjectMapper ) { private val mProgress = MutableLiveData() val progress: LiveData = mProgress - val showAll = MutableLiveData() - var selectedCurrency: String? = null - var selectedEvent: Transaction? = null + var selectedEvent: HistoryEvent? = null - val transactions: LiveData = showAll.switchMap { showAll -> - loadTransactions(showAll) - .onStart { mProgress.postValue(true) } - .onCompletion { mProgress.postValue(false) } - .asLiveData(Dispatchers.IO) - } + private val mTransactions = MutableLiveData() + val transactions: LiveData = mTransactions - private fun loadTransactions(showAll: Boolean) = callbackFlow { + fun loadTransactions() { + mProgress.postValue(true) walletBackendApi.sendRequest("getHistory", null) { isError, result -> - launch(Dispatchers.Default) { - if (isError) { - offer(TransactionsResult.Error) - close() - return@launch - } - val transactions = Transactions() - val json = result.getJSONArray("history") - val currency = selectedCurrency - for (i in 0 until json.length()) { - val event: Transaction = mapper.readValue(json.getString(i)) - event.json = json.getJSONObject(i) - if (currency == null || event.isCurrency(currency)) { - transactions.add(event) - } - } - transactions.reverse() // show latest first - val filtered = - if (showAll) transactions else transactions.filter { it.showToUser } as Transactions - offer(TransactionsResult.Success(filtered)) - close() + scope.launch(Dispatchers.Default) { + onTransactionsLoaded(isError, result) + } + } + } + + private fun onTransactionsLoaded(isError: Boolean, result: JSONObject) { + if (isError) { + mTransactions.postValue(TransactionsResult.Error) + return + } + val transactions = History() + val json = result.getJSONArray("history") + val currency = selectedCurrency + for (i in 0 until json.length()) { + val event: HistoryEvent = mapper.readValue(json.getString(i)) + if (event.showToUser && (currency == null || event.isCurrency(currency))) { + transactions.add(event) } } - awaitClose() + transactions.reverse() // show latest first + mProgress.postValue(false) + mTransactions.postValue(TransactionsResult.Success(transactions)) } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt index e7adaf1..2b5337f 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt @@ -42,17 +42,18 @@ import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.wallet.MainViewModel import net.taler.wallet.R +import net.taler.wallet.history.HistoryEvent -interface OnEventClickListener { - fun onTransactionClicked(transaction: Transaction) +interface OnTransactionClickListener { + fun onTransactionClicked(transaction: HistoryEvent) } -class TransactionsFragment : Fragment(), OnEventClickListener, ActionMode.Callback { +class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.Callback { private val model: MainViewModel by activityViewModels() private val transactionManager by lazy { model.transactionManager } - private val transactionAdapter by lazy { TransactionAdapter(model.devMode.value == true, this) } + private val transactionAdapter by lazy { TransactionAdapter(this) } private val currency by lazy { transactionManager.selectedCurrency!! } private var tracker: SelectionTracker? = null private var actionMode: ActionMode? = null @@ -109,7 +110,7 @@ class TransactionsFragment : Fragment(), OnEventClickListener, ActionMode.Callba }) // kicks off initial load, needs to be adapted if showAll state is ever saved - if (savedInstanceState == null) transactionManager.showAll.value = model.devMode.value + if (savedInstanceState == null) transactionManager.loadTransactions() } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -137,14 +138,11 @@ class TransactionsFragment : Fragment(), OnEventClickListener, ActionMode.Callba } } - override fun onTransactionClicked(transaction: Transaction) { + override fun onTransactionClicked(transaction: HistoryEvent) { if (actionMode != null) return // don't react on clicks while in action mode if (transaction.detailPageLayout != 0) { transactionManager.selectedEvent = transaction findNavController().navigate(R.id.action_nav_transaction_detail) - } else if (model.devMode.value == true) { - JsonDialogFragment.new(transaction.json.toString(2)) - .show(parentFragmentManager, null) } } diff --git a/wallet/src/main/res/drawable/ic_history.xml b/wallet/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..d9f75ea --- /dev/null +++ b/wallet/src/main/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/src/main/res/layout/fragment_transactions.xml b/wallet/src/main/res/layout/fragment_transactions.xml index aaf638c..547da24 100644 --- a/wallet/src/main/res/layout/fragment_transactions.xml +++ b/wallet/src/main/res/layout/fragment_transactions.xml @@ -27,7 +27,7 @@ android:scrollbars="vertical" android:visibility="invisible" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/list_item_transaction" + tools:listitem="@layout/list_item_history" tools:visibility="visible" /> + + + + + + + + + + + + diff --git a/wallet/src/main/res/layout/list_item_transaction.xml b/wallet/src/main/res/layout/list_item_transaction.xml deleted file mode 100644 index 2fabe1d..0000000 --- a/wallet/src/main/res/layout/list_item_transaction.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - diff --git a/wallet/src/main/res/menu/activity_main_drawer.xml b/wallet/src/main/res/menu/activity_main_drawer.xml index 896ff69..62abc32 100644 --- a/wallet/src/main/res/menu/activity_main_drawer.xml +++ b/wallet/src/main/res/menu/activity_main_drawer.xml @@ -18,7 +18,9 @@ xmlns:tools="http://schemas.android.com/tools" tools:showIn="@layout/activity_main"> - + - + + + + + + + + + diff --git a/wallet/src/main/res/navigation/nav_graph.xml b/wallet/src/main/res/navigation/nav_graph.xml index f8d515e..8e717c1 100644 --- a/wallet/src/main/res/navigation/nav_graph.xml +++ b/wallet/src/main/res/navigation/nav_graph.xml @@ -128,6 +128,12 @@ android:label="@string/pending_operations_title" tools:layout="@layout/fragment_pending_operations" /> + + + + diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index a28545f..56ff2ef 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card Withdraw Digital Cash Exchange\'s Terms of Service Exchange Fees + Event History Error Go Back diff --git a/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt b/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt new file mode 100644 index 0000000..109b8dc --- /dev/null +++ b/wallet/src/test/java/net/taler/wallet/history/HistoryEventTest.kt @@ -0,0 +1,488 @@ +/* + * 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.history + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import net.taler.common.Amount +import net.taler.wallet.history.ExchangeAddedEvent +import net.taler.wallet.history.ExchangeUpdatedEvent +import net.taler.wallet.history.HistoryEvent +import net.taler.wallet.history.OrderAcceptedHistoryEvent +import net.taler.wallet.history.OrderRedirectedHistoryEvent +import net.taler.wallet.history.OrderRefusedHistoryEvent +import net.taler.wallet.history.OrderShortInfo +import net.taler.wallet.history.PaymentAbortedHistoryEvent +import net.taler.wallet.history.PaymentHistoryEvent +import net.taler.wallet.history.RefreshHistoryEvent +import net.taler.wallet.history.RefreshReason.PAY +import net.taler.wallet.history.RefundHistoryEvent +import net.taler.wallet.history.ReserveBalanceUpdatedHistoryEvent +import net.taler.wallet.history.ReserveShortInfo +import net.taler.wallet.history.ReserveType.MANUAL +import net.taler.wallet.history.TipAcceptedHistoryEvent +import net.taler.wallet.history.TipDeclinedHistoryEvent +import net.taler.wallet.history.UnknownHistoryEvent +import net.taler.wallet.history.WithdrawHistoryEvent +import net.taler.wallet.history.WithdrawalSourceReserve +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + +class HistoryEventTest { + + private val mapper = ObjectMapper().registerModule(KotlinModule()) + + private val timestamp = Random.nextLong() + private val exchangeBaseUrl = "https://exchange.test.taler.net/" + private val orderShortInfo = OrderShortInfo( + proposalId = "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + orderId = "2019.364-01RAQ68DQ7AWR", + merchantBaseUrl = "https://backend.demo.taler.net/public/instances/FSF/", + amount = Amount.fromJSONString("KUDOS:0.5"), + summary = "Essay: Foreword" + ) + + @Test + fun `test ExchangeAddedEvent`() { + val builtIn = Random.nextBoolean() + val json = """{ + "type": "exchange-added", + "builtIn": $builtIn, + "eventId": "exchange-added;https%3A%2F%2Fexchange.test.taler.net%2F", + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val event: ExchangeAddedEvent = mapper.readValue(json) + + assertEquals(builtIn, event.builtIn) + assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test ExchangeUpdatedEvent`() { + val json = """{ + "type": "exchange-updated", + "eventId": "exchange-updated;https%3A%2F%2Fexchange.test.taler.net%2F", + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val event: ExchangeUpdatedEvent = mapper.readValue(json) + + assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test ReserveShortInfo`() { + val json = """{ + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "reserveCreationDetail": { + "type": "manual" + }, + "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" + }""".trimIndent() + val info: ReserveShortInfo = mapper.readValue(json) + + assertEquals(exchangeBaseUrl, info.exchangeBaseUrl) + assertEquals(MANUAL, info.reserveCreationDetail.type) + assertEquals("BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", info.reservePub) + } + + @Test + fun `test ReserveBalanceUpdatedTransaction`() { + val json = """{ + "type": "reserve-balance-updated", + "eventId": "reserve-balance-updated;K0H10Q6HB9WH0CKHQQMNH5C6GA7A9AR1E2XSS9G1KG3ZXMBVT26G", + "reserveAwaitedAmount": "TESTKUDOS:23", + "reserveUnclaimedAmount": "TESTKUDOS:0.01", + "reserveBalance": "TESTKUDOS:10", + "timestamp": { + "t_ms": $timestamp + }, + "reserveShortInfo": { + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "reserveCreationDetail": { + "type": "manual" + }, + "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" + } + }""".trimIndent() + val transaction: ReserveBalanceUpdatedHistoryEvent = mapper.readValue(json) + + assertEquals(timestamp, transaction.timestamp.ms) + assertEquals("TESTKUDOS:23", transaction.reserveAwaitedAmount.toJSONString()) + assertEquals("TESTKUDOS:10", transaction.reserveBalance.toJSONString()) + assertEquals("TESTKUDOS:0.01", transaction.reserveUnclaimedAmount.toJSONString()) + assertEquals(exchangeBaseUrl, transaction.reserveShortInfo.exchangeBaseUrl) + } + + @Test + fun `test WithdrawTransaction`() { + val json = """{ + "type": "withdrawn", + "withdrawalGroupId": "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", + "eventId": "withdrawn;974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", + "amountWithdrawnEffective": "TESTKUDOS:9.8", + "amountWithdrawnRaw": "TESTKUDOS:10", + "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", + "timestamp": { + "t_ms": $timestamp + }, + "withdrawalSource": { + "type": "reserve", + "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" + } + }""".trimIndent() + val event: WithdrawHistoryEvent = mapper.readValue(json) + + assertEquals( + "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", + event.withdrawalGroupId + ) + assertEquals("TESTKUDOS:9.8", event.amountWithdrawnEffective.toJSONString()) + assertEquals("TESTKUDOS:10", event.amountWithdrawnRaw.toJSONString()) + assertTrue(event.withdrawalSource is WithdrawalSourceReserve) + assertEquals( + "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", + (event.withdrawalSource as WithdrawalSourceReserve).reservePub + ) + assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test OrderShortInfo`() { + val json = """{ + "amount": "KUDOS:0.5", + "orderId": "2019.364-01RAQ68DQ7AWR", + "merchantBaseUrl": "https:\/\/backend.demo.taler.net\/public\/instances\/FSF\/", + "proposalId": "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "summary": "Essay: Foreword" + }""".trimIndent() + val info: OrderShortInfo = mapper.readValue(json) + + assertEquals("KUDOS:0.5", info.amount.toJSONString()) + assertEquals("2019.364-01RAQ68DQ7AWR", info.orderId) + assertEquals("Essay: Foreword", info.summary) + } + + @Test + fun `test OrderAcceptedTransaction`() { + val json = """{ + "type": "order-accepted", + "eventId": "order-accepted;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val transaction: OrderAcceptedHistoryEvent = mapper.readValue(json) + + assertEquals(orderShortInfo, transaction.orderShortInfo) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test OrderRefusedTransaction`() { + val json = """{ + "type": "order-refused", + "eventId": "order-refused;9RJGAYXKWX0Y3V37H66606SXSA7V2CV255EBFS4G1JSH6W1EG7F0", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val transaction: OrderRefusedHistoryEvent = mapper.readValue(json) + + assertEquals(orderShortInfo, transaction.orderShortInfo) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test PaymentTransaction`() { + val json = """{ + "type": "payment-sent", + "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "replay": false, + "sessionId": "e4f436c4-3c5c-4aee-81d2-26e425c09520", + "timestamp": { + "t_ms": $timestamp + }, + "numCoins": 6, + "amountPaidWithFees": "KUDOS:0.6" + }""".trimIndent() + val event: PaymentHistoryEvent = mapper.readValue(json) + + assertEquals(orderShortInfo, event.orderShortInfo) + assertEquals(false, event.replay) + assertEquals(6, event.numCoins) + assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) + assertEquals("e4f436c4-3c5c-4aee-81d2-26e425c09520", event.sessionId) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test PaymentTransaction without sessionId`() { + val json = """{ + "type": "payment-sent", + "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "replay": true, + "timestamp": { + "t_ms": $timestamp + }, + "numCoins": 6, + "amountPaidWithFees": "KUDOS:0.6" + }""".trimIndent() + val event: PaymentHistoryEvent = mapper.readValue(json) + + assertEquals(orderShortInfo, event.orderShortInfo) + assertEquals(true, event.replay) + assertEquals(6, event.numCoins) + assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) + assertEquals(null, event.sessionId) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test PaymentAbortedTransaction`() { + val json = """{ + "type": "payment-aborted", + "eventId": "payment-sent;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + }, + "amountLost": "KUDOS:0.1" + }""".trimIndent() + val transaction: PaymentAbortedHistoryEvent = mapper.readValue(json) + + assertEquals(orderShortInfo, transaction.orderShortInfo) + assertEquals("KUDOS:0.1", transaction.amountLost.toJSONString()) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test TipAcceptedTransaction`() { + val json = """{ + "type": "tip-accepted", + "timestamp": { + "t_ms": $timestamp + }, + "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipRaw": "KUDOS:4" + }""".trimIndent() + val transaction: TipAcceptedHistoryEvent = mapper.readValue(json) + + assertEquals( + "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + transaction.tipId + ) + assertEquals("KUDOS:4", transaction.tipRaw.toJSONString()) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test TipDeclinedTransaction`() { + val json = """{ + "type": "tip-declined", + "timestamp": { + "t_ms": $timestamp + }, + "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipId": "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "tipAmount": "KUDOS:4" + }""".trimIndent() + val transaction: TipDeclinedHistoryEvent = mapper.readValue(json) + + assertEquals( + "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + transaction.tipId + ) + assertEquals("KUDOS:4", transaction.tipAmount.toJSONString()) + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test RefundTransaction`() { + val json = """{ + "type": "refund", + "eventId": "refund;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "refundGroupId": "refund;998724", + "orderShortInfo": { + "amount": "${orderShortInfo.amount.toJSONString()}", + "orderId": "${orderShortInfo.orderId}", + "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", + "proposalId": "${orderShortInfo.proposalId}", + "summary": "${orderShortInfo.summary}" + }, + "timestamp": { + "t_ms": $timestamp + }, + "amountRefundedRaw": "KUDOS:1", + "amountRefundedInvalid": "KUDOS:0.5", + "amountRefundedEffective": "KUDOS:0.4" + }""".trimIndent() + val event: RefundHistoryEvent = mapper.readValue(json) + + assertEquals("refund;998724", event.refundGroupId) + assertEquals("KUDOS:1", event.amountRefundedRaw.toJSONString()) + assertEquals("KUDOS:0.5", event.amountRefundedInvalid.toJSONString()) + assertEquals("KUDOS:0.4", event.amountRefundedEffective.toJSONString()) + assertEquals(orderShortInfo, event.orderShortInfo) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test RefreshTransaction`() { + val json = """{ + "type": "refreshed", + "refreshGroupId": "8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", + "eventId": "refreshed;8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", + "timestamp": { + "t_ms": $timestamp + }, + "refreshReason": "pay", + "amountRefreshedEffective": "KUDOS:0", + "amountRefreshedRaw": "KUDOS:1", + "numInputCoins": 6, + "numOutputCoins": 0, + "numRefreshedInputCoins": 1 + }""".trimIndent() + val event: RefreshHistoryEvent = mapper.readValue(json) + + assertEquals("KUDOS:0", event.amountRefreshedEffective.toJSONString()) + assertEquals("KUDOS:1", event.amountRefreshedRaw.toJSONString()) + assertEquals(6, event.numInputCoins) + assertEquals(0, event.numOutputCoins) + assertEquals(1, event.numRefreshedInputCoins) + assertEquals("8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", event.refreshGroupId) + assertEquals(PAY, event.refreshReason) + assertEquals(timestamp, event.timestamp.ms) + } + + @Test + fun `test OrderRedirectedTransaction`() { + val json = """{ + "type": "order-redirected", + "eventId": "order-redirected;621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", + "alreadyPaidOrderShortInfo": { + "amount": "KUDOS:0.5", + "orderId": "2019.354-01P25CD66P8NG", + "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", + "proposalId": "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + "summary": "Essay: 1. The Free Software Definition" + }, + "newOrderShortInfo": { + "amount": "KUDOS:0.5", + "orderId": "2019.364-01M4QH6KPMJY4", + "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", + "proposalId": "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", + "summary": "Essay: 1. The Free Software Definition" + }, + "timestamp": { + "t_ms": $timestamp + } + }""".trimIndent() + val transaction: OrderRedirectedHistoryEvent = mapper.readValue(json) + + assertEquals( + "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", + transaction.alreadyPaidOrderShortInfo.proposalId + ) + assertEquals( + "https://backend.demo.taler.net/public/instances/FSF/", + transaction.alreadyPaidOrderShortInfo.merchantBaseUrl + ) + assertEquals("2019.354-01P25CD66P8NG", transaction.alreadyPaidOrderShortInfo.orderId) + assertEquals("KUDOS:0.5", transaction.alreadyPaidOrderShortInfo.amount.toJSONString()) + assertEquals( + "Essay: 1. The Free Software Definition", + transaction.alreadyPaidOrderShortInfo.summary + ) + + assertEquals( + "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", + transaction.newOrderShortInfo.proposalId + ) + assertEquals( + "https://backend.demo.taler.net/public/instances/FSF/", + transaction.newOrderShortInfo.merchantBaseUrl + ) + assertEquals("2019.364-01M4QH6KPMJY4", transaction.newOrderShortInfo.orderId) + assertEquals("KUDOS:0.5", transaction.newOrderShortInfo.amount.toJSONString()) + assertEquals("Essay: 1. The Free Software Definition", transaction.newOrderShortInfo.summary) + + assertEquals(timestamp, transaction.timestamp.ms) + } + + @Test + fun `test UnknownTransaction`() { + val json = """{ + "type": "does not exist", + "timestamp": { + "t_ms": $timestamp + }, + "eventId": "does-not-exist;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0" + }""".trimIndent() + val event: HistoryEvent = mapper.readValue(json) + + assertEquals(UnknownHistoryEvent::class.java, event.javaClass) + assertEquals(timestamp, event.timestamp.ms) + } + +} diff --git a/wallet/src/test/java/net/taler/wallet/history/ReserveHistoryEventTest.kt b/wallet/src/test/java/net/taler/wallet/history/ReserveHistoryEventTest.kt new file mode 100644 index 0000000..f09d7b6 --- /dev/null +++ b/wallet/src/test/java/net/taler/wallet/history/ReserveHistoryEventTest.kt @@ -0,0 +1,53 @@ +/* + * 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.history + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import net.taler.wallet.history.ReserveDepositTransaction +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.random.Random + +class ReserveHistoryEventTest { + + private val mapper = ObjectMapper().registerModule(KotlinModule()) + + private val timestamp = Random.nextLong() + + @Test + fun `test ExchangeAddedEvent`() { + val senderAccountUrl = "payto://x-taler-bank/bank.test.taler.net/894" + val json = """{ + "amount": "TESTKUDOS:10", + "sender_account_url": "payto:\/\/x-taler-bank\/bank.test.taler.net\/894", + "timestamp": { + "t_ms": $timestamp + }, + "wire_reference": "00000000004TR", + "type": "DEPOSIT" + }""".trimIndent() + val transaction: ReserveDepositTransaction = mapper.readValue(json) + + assertEquals("TESTKUDOS:10", transaction.amount) + assertEquals(senderAccountUrl, transaction.senderAccountUrl) + assertEquals("00000000004TR", transaction.wireReference) + assertEquals(timestamp, transaction.timestamp.ms) + } + +} diff --git a/wallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt b/wallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt deleted file mode 100644 index 4a3c75b..0000000 --- a/wallet/src/test/java/net/taler/wallet/transactions/ReserveTransactionTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.readValue -import org.junit.Assert.assertEquals -import org.junit.Test -import kotlin.random.Random - -class ReserveTransactionTest { - - private val mapper = ObjectMapper().registerModule(KotlinModule()) - - private val timestamp = Random.nextLong() - - @Test - fun `test ExchangeAddedEvent`() { - val senderAccountUrl = "payto://x-taler-bank/bank.test.taler.net/894" - val json = """{ - "amount": "TESTKUDOS:10", - "sender_account_url": "payto:\/\/x-taler-bank\/bank.test.taler.net\/894", - "timestamp": { - "t_ms": $timestamp - }, - "wire_reference": "00000000004TR", - "type": "DEPOSIT" - }""".trimIndent() - val transaction: ReserveDepositTransaction = mapper.readValue(json) - - assertEquals("TESTKUDOS:10", transaction.amount) - assertEquals(senderAccountUrl, transaction.senderAccountUrl) - assertEquals("00000000004TR", transaction.wireReference) - assertEquals(timestamp, transaction.timestamp.ms) - } - -} diff --git a/wallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt b/wallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt deleted file mode 100644 index 6549434..0000000 --- a/wallet/src/test/java/net/taler/wallet/transactions/TransactionTest.kt +++ /dev/null @@ -1,470 +0,0 @@ -/* - * 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 com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.readValue -import net.taler.common.Amount -import net.taler.wallet.transactions.RefreshReason.PAY -import net.taler.wallet.transactions.ReserveType.MANUAL -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import kotlin.random.Random - -class TransactionTest { - - private val mapper = ObjectMapper().registerModule(KotlinModule()) - - private val timestamp = Random.nextLong() - private val exchangeBaseUrl = "https://exchange.test.taler.net/" - private val orderShortInfo = OrderShortInfo( - proposalId = "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - orderId = "2019.364-01RAQ68DQ7AWR", - merchantBaseUrl = "https://backend.demo.taler.net/public/instances/FSF/", - amount = Amount.fromJSONString("KUDOS:0.5"), - summary = "Essay: Foreword" - ) - - @Test - fun `test ExchangeAddedEvent`() { - val builtIn = Random.nextBoolean() - val json = """{ - "type": "exchange-added", - "builtIn": $builtIn, - "eventId": "exchange-added;https%3A%2F%2Fexchange.test.taler.net%2F", - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: ExchangeAddedEvent = mapper.readValue(json) - - assertEquals(builtIn, event.builtIn) - assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test ExchangeUpdatedEvent`() { - val json = """{ - "type": "exchange-updated", - "eventId": "exchange-updated;https%3A%2F%2Fexchange.test.taler.net%2F", - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val event: ExchangeUpdatedEvent = mapper.readValue(json) - - assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test ReserveShortInfo`() { - val json = """{ - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "reserveCreationDetail": { - "type": "manual" - }, - "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" - }""".trimIndent() - val info: ReserveShortInfo = mapper.readValue(json) - - assertEquals(exchangeBaseUrl, info.exchangeBaseUrl) - assertEquals(MANUAL, info.reserveCreationDetail.type) - assertEquals("BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", info.reservePub) - } - - @Test - fun `test ReserveBalanceUpdatedTransaction`() { - val json = """{ - "type": "reserve-balance-updated", - "eventId": "reserve-balance-updated;K0H10Q6HB9WH0CKHQQMNH5C6GA7A9AR1E2XSS9G1KG3ZXMBVT26G", - "reserveAwaitedAmount": "TESTKUDOS:23", - "reserveUnclaimedAmount": "TESTKUDOS:0.01", - "reserveBalance": "TESTKUDOS:10", - "timestamp": { - "t_ms": $timestamp - }, - "reserveShortInfo": { - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "reserveCreationDetail": { - "type": "manual" - }, - "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" - } - }""".trimIndent() - val transaction: ReserveBalanceUpdatedTransaction = mapper.readValue(json) - - assertEquals(timestamp, transaction.timestamp.ms) - assertEquals("TESTKUDOS:23", transaction.reserveAwaitedAmount.toJSONString()) - assertEquals("TESTKUDOS:10", transaction.reserveBalance.toJSONString()) - assertEquals("TESTKUDOS:0.01", transaction.reserveUnclaimedAmount.toJSONString()) - assertEquals(exchangeBaseUrl, transaction.reserveShortInfo.exchangeBaseUrl) - } - - @Test - fun `test WithdrawTransaction`() { - val json = """{ - "type": "withdrawn", - "withdrawalGroupId": "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", - "eventId": "withdrawn;974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", - "amountWithdrawnEffective": "TESTKUDOS:9.8", - "amountWithdrawnRaw": "TESTKUDOS:10", - "exchangeBaseUrl": "https:\/\/exchange.test.taler.net\/", - "timestamp": { - "t_ms": $timestamp - }, - "withdrawalSource": { - "type": "reserve", - "reservePub": "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G" - } - }""".trimIndent() - val event: WithdrawTransaction = mapper.readValue(json) - - assertEquals( - "974FT7JDNR20EQKNR21G1HV9PB6T5AZHYHX9NHR51Q30ZK3T10S0", - event.withdrawalGroupId - ) - assertEquals("TESTKUDOS:9.8", event.amountWithdrawnEffective.toJSONString()) - assertEquals("TESTKUDOS:10", event.amountWithdrawnRaw.toJSONString()) - assertTrue(event.withdrawalSource is WithdrawalSourceReserve) - assertEquals( - "BRT2P0YMQSD5F48V9XHVNH73ZTS6EZC0KCQCPGPZQWTSQB77615G", - (event.withdrawalSource as WithdrawalSourceReserve).reservePub - ) - assertEquals(exchangeBaseUrl, event.exchangeBaseUrl) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test OrderShortInfo`() { - val json = """{ - "amount": "KUDOS:0.5", - "orderId": "2019.364-01RAQ68DQ7AWR", - "merchantBaseUrl": "https:\/\/backend.demo.taler.net\/public\/instances\/FSF\/", - "proposalId": "EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "summary": "Essay: Foreword" - }""".trimIndent() - val info: OrderShortInfo = mapper.readValue(json) - - assertEquals("KUDOS:0.5", info.amount.toJSONString()) - assertEquals("2019.364-01RAQ68DQ7AWR", info.orderId) - assertEquals("Essay: Foreword", info.summary) - } - - @Test - fun `test OrderAcceptedTransaction`() { - val json = """{ - "type": "order-accepted", - "eventId": "order-accepted;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val transaction: OrderAcceptedTransaction = mapper.readValue(json) - - assertEquals(orderShortInfo, transaction.orderShortInfo) - assertEquals(timestamp, transaction.timestamp.ms) - } - - @Test - fun `test OrderRefusedTransaction`() { - val json = """{ - "type": "order-refused", - "eventId": "order-refused;9RJGAYXKWX0Y3V37H66606SXSA7V2CV255EBFS4G1JSH6W1EG7F0", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val transaction: OrderRefusedTransaction = mapper.readValue(json) - - assertEquals(orderShortInfo, transaction.orderShortInfo) - assertEquals(timestamp, transaction.timestamp.ms) - } - - @Test - fun `test PaymentTransaction`() { - val json = """{ - "type": "payment-sent", - "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "replay": false, - "sessionId": "e4f436c4-3c5c-4aee-81d2-26e425c09520", - "timestamp": { - "t_ms": $timestamp - }, - "numCoins": 6, - "amountPaidWithFees": "KUDOS:0.6" - }""".trimIndent() - val event: PaymentTransaction = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(false, event.replay) - assertEquals(6, event.numCoins) - assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) - assertEquals("e4f436c4-3c5c-4aee-81d2-26e425c09520", event.sessionId) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test PaymentTransaction without sessionId`() { - val json = """{ - "type": "payment-sent", - "eventId": "payment-sent;EP5MH4R5C9RMNA06YS1QGEJ3EY682PY8R1SGRFRP74EV735N3ATG", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "replay": true, - "timestamp": { - "t_ms": $timestamp - }, - "numCoins": 6, - "amountPaidWithFees": "KUDOS:0.6" - }""".trimIndent() - val event: PaymentTransaction = mapper.readValue(json) - - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(true, event.replay) - assertEquals(6, event.numCoins) - assertEquals("KUDOS:0.6", event.amountPaidWithFees.toJSONString()) - assertEquals(null, event.sessionId) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test PaymentAbortedTransaction`() { - val json = """{ - "type": "payment-aborted", - "eventId": "payment-sent;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - }, - "amountLost": "KUDOS:0.1" - }""".trimIndent() - val transaction: PaymentAbortedTransaction = mapper.readValue(json) - - assertEquals(orderShortInfo, transaction.orderShortInfo) - assertEquals("KUDOS:0.1", transaction.amountLost.toJSONString()) - assertEquals(timestamp, transaction.timestamp.ms) - } - - @Test - fun `test TipAcceptedTransaction`() { - val json = """{ - "type": "tip-accepted", - "timestamp": { - "t_ms": $timestamp - }, - "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipRaw": "KUDOS:4" - }""".trimIndent() - val transaction: TipAcceptedTransaction = mapper.readValue(json) - - assertEquals( - "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - transaction.tipId - ) - assertEquals("KUDOS:4", transaction.tipRaw.toJSONString()) - assertEquals(timestamp, transaction.timestamp.ms) - } - - @Test - fun `test TipDeclinedTransaction`() { - val json = """{ - "type": "tip-declined", - "timestamp": { - "t_ms": $timestamp - }, - "eventId": "tip-accepted;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipId": "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "tipAmount": "KUDOS:4" - }""".trimIndent() - val transaction: TipDeclinedTransaction = mapper.readValue(json) - - assertEquals( - "tip-accepted;998724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - transaction.tipId - ) - assertEquals("KUDOS:4", transaction.tipAmount.toJSONString()) - assertEquals(timestamp, transaction.timestamp.ms) - } - - @Test - fun `test RefundTransaction`() { - val json = """{ - "type": "refund", - "eventId": "refund;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "refundGroupId": "refund;998724", - "orderShortInfo": { - "amount": "${orderShortInfo.amount.toJSONString()}", - "orderId": "${orderShortInfo.orderId}", - "merchantBaseUrl": "${orderShortInfo.merchantBaseUrl}", - "proposalId": "${orderShortInfo.proposalId}", - "summary": "${orderShortInfo.summary}" - }, - "timestamp": { - "t_ms": $timestamp - }, - "amountRefundedRaw": "KUDOS:1", - "amountRefundedInvalid": "KUDOS:0.5", - "amountRefundedEffective": "KUDOS:0.4" - }""".trimIndent() - val event: RefundTransaction = mapper.readValue(json) - - assertEquals("refund;998724", event.refundGroupId) - assertEquals("KUDOS:1", event.amountRefundedRaw.toJSONString()) - assertEquals("KUDOS:0.5", event.amountRefundedInvalid.toJSONString()) - assertEquals("KUDOS:0.4", event.amountRefundedEffective.toJSONString()) - assertEquals(orderShortInfo, event.orderShortInfo) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test RefreshTransaction`() { - val json = """{ - "type": "refreshed", - "refreshGroupId": "8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", - "eventId": "refreshed;8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", - "timestamp": { - "t_ms": $timestamp - }, - "refreshReason": "pay", - "amountRefreshedEffective": "KUDOS:0", - "amountRefreshedRaw": "KUDOS:1", - "numInputCoins": 6, - "numOutputCoins": 0, - "numRefreshedInputCoins": 1 - }""".trimIndent() - val event: RefreshTransaction = mapper.readValue(json) - - assertEquals("KUDOS:0", event.amountRefreshedEffective.toJSONString()) - assertEquals("KUDOS:1", event.amountRefreshedRaw.toJSONString()) - assertEquals(6, event.numInputCoins) - assertEquals(0, event.numOutputCoins) - assertEquals(1, event.numRefreshedInputCoins) - assertEquals("8AVHKJFAN4QV4C11P56NEY83AJMGFF2KF412AN3Y0QBP09RSN640", event.refreshGroupId) - assertEquals(PAY, event.refreshReason) - assertEquals(timestamp, event.timestamp.ms) - } - - @Test - fun `test OrderRedirectedTransaction`() { - val json = """{ - "type": "order-redirected", - "eventId": "order-redirected;621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", - "alreadyPaidOrderShortInfo": { - "amount": "KUDOS:0.5", - "orderId": "2019.354-01P25CD66P8NG", - "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", - "proposalId": "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - "summary": "Essay: 1. The Free Software Definition" - }, - "newOrderShortInfo": { - "amount": "KUDOS:0.5", - "orderId": "2019.364-01M4QH6KPMJY4", - "merchantBaseUrl": "https://backend.demo.taler.net/public/instances/FSF/", - "proposalId": "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", - "summary": "Essay: 1. The Free Software Definition" - }, - "timestamp": { - "t_ms": $timestamp - } - }""".trimIndent() - val transaction: OrderRedirectedTransaction = mapper.readValue(json) - - assertEquals( - "898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0", - transaction.alreadyPaidOrderShortInfo.proposalId - ) - assertEquals( - "https://backend.demo.taler.net/public/instances/FSF/", - transaction.alreadyPaidOrderShortInfo.merchantBaseUrl - ) - assertEquals("2019.354-01P25CD66P8NG", transaction.alreadyPaidOrderShortInfo.orderId) - assertEquals("KUDOS:0.5", transaction.alreadyPaidOrderShortInfo.amount.toJSONString()) - assertEquals( - "Essay: 1. The Free Software Definition", - transaction.alreadyPaidOrderShortInfo.summary - ) - - assertEquals( - "621J6D5SXG7M17TYA26945DYKNQZPW4600MZ1W8MADA1RRR49F8G", - transaction.newOrderShortInfo.proposalId - ) - assertEquals( - "https://backend.demo.taler.net/public/instances/FSF/", - transaction.newOrderShortInfo.merchantBaseUrl - ) - assertEquals("2019.364-01M4QH6KPMJY4", transaction.newOrderShortInfo.orderId) - assertEquals("KUDOS:0.5", transaction.newOrderShortInfo.amount.toJSONString()) - assertEquals("Essay: 1. The Free Software Definition", transaction.newOrderShortInfo.summary) - - assertEquals(timestamp, transaction.timestamp.ms) - } - - @Test - fun `test UnknownTransaction`() { - val json = """{ - "type": "does not exist", - "timestamp": { - "t_ms": $timestamp - }, - "eventId": "does-not-exist;898724XGQ1GGMZB4WY3KND582NSP74FZ60BX0Y87FF81H0FJ8XD0" - }""".trimIndent() - val event: Transaction = mapper.readValue(json) - - assertEquals(UnknownTransaction::class.java, event.javaClass) - assertEquals(timestamp, event.timestamp.ms) - } - -} -- cgit v1.2.3