TransactionState.kt
/*
* This file is part of GNU Taler
* (C) 2023 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 <http://www.gnu.org/licenses/>
*/
package net.taler.wallet.transactions
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializable(with = TransactionStateSerializer::class)
data class TransactionState(
val major: TransactionMajorState,
val minor: TransactionMinorState? = null,
/**
* Exact serialized name of [major] as received from wallet-core.
* Only set when [major] is [TransactionMajorState.Unknown], so that
* unrecognized/future states survive a serialize round trip.
*/
val majorName: String? = null,
/**
* Exact serialized name of [minor] as received from wallet-core.
* Only set when [minor] is [TransactionMinorState.Unknown].
*/
val minorName: String? = null,
) {
override fun equals(other: Any?): Boolean {
return if (other is TransactionState)
// if other.minor is null, then ignore minor in comparison
major == other.major && (other.minor == null || minor == other.minor)
else false
}
override fun hashCode(): Int {
var result = major.hashCode()
result = 31 * result + (minor?.hashCode() ?: 0)
return result
}
}
@Serializable(with = TransactionMajorStateSerializer::class)
enum class TransactionMajorState(val serialName: String) {
Unknown("unknown"),
None("none"),
Pending("pending"),
Done("done"),
Aborting("aborting"),
Aborted("aborted"),
Dialog("dialog"),
Finalizing("finalizing"),
Suspended("suspended"),
SuspendedFinalizing("suspended-finalizing"),
SuspendedAborting("suspended-aborting"),
Failed("failed"),
Expired("expired"),
Deleted("deleted"),
}
object TransactionMajorStateSerializer : KSerializer<TransactionMajorState> {
private val serialNames = TransactionMajorState.entries.associateBy { it.serialName }
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("TransactionMajorState", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): TransactionMajorState =
fromName(decoder.decodeString())
override fun serialize(encoder: Encoder, value: TransactionMajorState) {
encoder.encodeString(value.serialName)
}
fun fromName(serialName: String): TransactionMajorState =
serialNames[serialName] ?: TransactionMajorState.Unknown
}
@Serializable(with = TransactionMinorStateSerializer::class)
enum class TransactionMinorState(val serialName: String) {
AbortingBank("aborting-bank"),
AcceptRefund("accept-refund"),
AutoRefund("auto-refund"),
BalanceKycRequired("balance-kyc"),
Bank("bank"),
BankConfirmTransfer("bank-confirm-transfer"),
BankRegisterReserve("bank-register-reserve"),
CheckRefund("check-refund"),
ClaimProposal("claim-proposal"),
CompletedByOtherWallet("completed-by-other-wallet"),
CreatePurse("create-purse"),
DeletePurse("delete-purse"),
Deposit("deposit"),
Exchange("exchange"),
ExchangeWaitReserve("exchange-wait-reserve"),
KycAuthRequired("kyc-auth"),
KycInit("kyc-init"),
KycRequired("kyc"),
Merge("merge"),
PaidByOther("paid-by-other"),
Proposed("proposed"),
Ready("ready"),
RebindSession("rebind-session"),
Refresh("refresh"),
Refused("refused"),
Repurchase("repurchase"),
SubmitPayment("submit-payment"),
Track("track"),
Unknown("unknown"),
Withdraw("withdraw"),
Abort("abort"),
}
object TransactionMinorStateSerializer : KSerializer<TransactionMinorState> {
private val serialNames = TransactionMinorState.entries.associateBy { it.serialName }
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("TransactionMinorState", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): TransactionMinorState =
fromName(decoder.decodeString())
override fun serialize(encoder: Encoder, value: TransactionMinorState) {
encoder.encodeString(value.serialName)
}
fun fromName(serialName: String): TransactionMinorState =
serialNames[serialName] ?: TransactionMinorState.Unknown
}
object TransactionStateSerializer : KSerializer<TransactionState> {
override val descriptor: SerialDescriptor = JsonObject.serializer().descriptor
override fun deserialize(decoder: Decoder): TransactionState {
val jsonDecoder = decoder as? JsonDecoder
?: error("TransactionStateSerializer can only be used with Json")
val obj = jsonDecoder.decodeJsonElement().jsonObject
val majorName = obj["major"]?.jsonPrimitive?.contentOrNull ?: "unknown"
val minorName = obj["minor"]?.jsonPrimitive?.contentOrNull
return TransactionState(
major = TransactionMajorStateSerializer.fromName(majorName),
minor = minorName?.let(TransactionMinorStateSerializer::fromName),
majorName = majorName,
minorName = minorName,
)
}
override fun serialize(encoder: Encoder, value: TransactionState) {
val jsonEncoder = encoder as? JsonEncoder
?: error("TransactionStateSerializer can only be used with Json")
jsonEncoder.encodeJsonElement(buildJsonObject {
put(
"major",
JsonPrimitive(
if (value.major == TransactionMajorState.Unknown && value.majorName != null) value.majorName
else value.major.serialName,
),
)
val minorName = value.minor?.let { minor ->
if (minor == TransactionMinorState.Unknown && value.minorName != null) value.minorName
else minor.serialName
} ?: value.minorName
if (minorName != null) put("minor", JsonPrimitive(minorName))
})
}
}
/**
* Whether the difference between a credit's raw and effective amounts is a
* fee. For transactions that never completed, wallet-core zeroes the
* effective amount, which would otherwise make the whole amount look like a
* fee.
*/
val Transaction.feeIsMeaningful: Boolean
get() = when (txState.major) {
TransactionMajorState.Aborted,
TransactionMajorState.Aborting,
TransactionMajorState.SuspendedAborting,
TransactionMajorState.Failed,
TransactionMajorState.Expired,
TransactionMajorState.Deleted -> false
else -> true
}