Notifications.kt
/*
* This file is part of GNU Taler
* (C) 2026 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.backend
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.taler.common.RelativeTime
import net.taler.wallet.events.ObservabilityEvent
import net.taler.wallet.transactions.TransactionState
@Serializable(with = NotificationPayloadSerializer::class)
sealed class NotificationPayload {
@Serializable
@SerialName("balance-change")
data class BalanceChange(
/**
* If set to true, the balance change is internal
* to the wallet and not visible to the user.
*/
val isInternal: Boolean? = null,
val hintTransactionId: String? = null,
) : NotificationPayload()
@Serializable
@SerialName("transaction-state-transition")
data class TransactionStateTransition(
val transactionId: String? = null,
val causeHint: String? = null,
val oldTxState: TransactionState? = null,
val newTxState: TransactionState? = null,
val errorInfo: TalerErrorInfo? = null,
) : NotificationPayload()
@Serializable
@SerialName("task-observability-event")
data class TaskObservabilityEvent(
val taskId: String? = null,
val event: ObservabilityEvent? = null,
) : NotificationPayload()
@Serializable
@SerialName("request-observability-event")
data class RequestObservabilityEvent(
val requestId: Int,
val operation: String? = null,
val event: ObservabilityEvent? = null,
) : NotificationPayload()
@Serializable
@SerialName("database-maintenance-progress")
data class DatabaseMaintenanceProgress(
val operation: String,
val phase: String,
val progressToken: String? = null,
val completionPercent: Int? = null,
val error: TalerErrorInfo? = null,
) : NotificationPayload()
@Serializable
@SerialName("request-progress-phase")
data class RequestProgressPhase(
val operation: String,
val progressToken: String,
val phase: ProgressPhase,
) : NotificationPayload()
@Serializable
@SerialName("request-progress-error")
data class RequestProgressError(
val operation: String,
val progressToken: String,
val error: TalerErrorInfo,
val nextRetryDelay: RelativeTime? = null,
val retryCounter: Int? = null,
) : NotificationPayload()
@Serializable(with = UnknownPayloadSerializer::class)
data class Unknown(
val type: String? = null,
val raw: JsonObject,
) : NotificationPayload()
}
@Serializable
enum class ProgressPhase {
@SerialName("delayed")
Delayed,
@SerialName("stalled")
Stalled,
@SerialName("done")
Done,
}
object NotificationPayloadSerializer :
JsonContentPolymorphicSerializer<NotificationPayload>(NotificationPayload::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<NotificationPayload> {
val type = element.jsonObject["type"]?.jsonPrimitive?.contentOrNull
val serializer: DeserializationStrategy<NotificationPayload> = when (type) {
"balance-change" -> NotificationPayload.BalanceChange.serializer()
"transaction-state-transition" -> NotificationPayload.TransactionStateTransition.serializer()
"task-observability-event" -> NotificationPayload.TaskObservabilityEvent.serializer()
"request-observability-event" -> NotificationPayload.RequestObservabilityEvent.serializer()
"database-maintenance-progress" -> NotificationPayload.DatabaseMaintenanceProgress.serializer()
"request-progress-phase" -> NotificationPayload.RequestProgressPhase.serializer()
"request-progress-error" -> NotificationPayload.RequestProgressError.serializer()
else -> NotificationPayload.Unknown.serializer()
}
return serializer
}
}
object UnknownPayloadSerializer : KSerializer<NotificationPayload.Unknown> {
// Delegate descriptor to JsonObject.serializer().descriptor so kotlinx.serialization
// knows this serializer operates directly on a raw JSON object structure.
override val descriptor: SerialDescriptor = JsonObject.serializer().descriptor
override fun deserialize(decoder: Decoder): NotificationPayload.Unknown {
val jsonDecoder = decoder as? JsonDecoder
?: error("UnknownPayloadSerializer can only be used with Json")
val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
val type = jsonObject["type"]?.jsonPrimitive?.contentOrNull
return NotificationPayload.Unknown(
type = type,
raw = jsonObject,
)
}
override fun serialize(encoder: Encoder, value: NotificationPayload.Unknown) {
val jsonEncoder = encoder as? JsonEncoder
?: error("UnknownPayloadSerializer can only be used with Json")
jsonEncoder.encodeJsonElement(value.raw)
}
}