ContractTerms.kt
/*
* 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 <http://www.gnu.org/licenses/>
*/
package net.taler.common
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializable
enum class ContractVersion(val version: Int) {
V0(0),
V1(1),
}
@OptIn(ExperimentalSerializationApi::class)
@Serializable(with = ContractTermsSerializer::class)
sealed class ContractTerms {
abstract val version: ContractVersion
abstract val summary: String
abstract val summaryI18n: Map<String, String>?
abstract val orderId: String
abstract val fulfillmentUrl: String?
abstract val fulfillmentMessage: String?
abstract val fulfillmentMessageI18n: Map <String, String>?
abstract val products: List<ContractProduct>?
abstract val refundDeadline: Timestamp?
abstract val payDeadline: Timestamp?
abstract val wireTransferDeadline: Timestamp?
abstract val merchantBaseUrl: String
abstract val merchant: Merchant
abstract val exchanges: List<Exchange>
abstract val deliveryLocation: Location?
abstract val deliveryDate: Timestamp?
@Serializable
@JsonClassDiscriminator("version")
data class V0 (
override val summary: String,
@SerialName("summary_i18n")
override val summaryI18n: Map<String, String>? = null,
@SerialName("order_id")
override val orderId: String,
@SerialName("fulfillment_url")
override val fulfillmentUrl: String? = null,
@SerialName("fulfillment_message")
override val fulfillmentMessage: String? = null,
@SerialName("fulfillment_message_i18n")
override val fulfillmentMessageI18n: Map<String, String>? = null,
override val products: List<ContractProduct>? = null,
@SerialName("refund_deadline")
override val refundDeadline: Timestamp? = null,
@SerialName("pay_deadline")
override val payDeadline: Timestamp? = null,
@SerialName("wire_transfer_deadline")
override val wireTransferDeadline: Timestamp? = null,
@SerialName("merchant_base_url")
override val merchantBaseUrl: String,
override val merchant: Merchant,
override val exchanges: List<Exchange> = listOf(),
@SerialName("delivery_location")
override val deliveryLocation: Location? = null,
@SerialName("delivery_date")
override val deliveryDate: Timestamp? = null,
val amount: Amount,
@SerialName("max_fee")
val maxFee: Amount,
) : ContractTerms() {
override val version: ContractVersion = ContractVersion.V0
}
@Serializable
@JsonClassDiscriminator("version")
data class V1 (
override val summary: String,
@SerialName("summary_i18n")
override val summaryI18n: Map<String, String>? = null,
@SerialName("order_id")
override val orderId: String,
@SerialName("fulfillment_url")
override val fulfillmentUrl: String? = null,
@SerialName("fulfillment_message")
override val fulfillmentMessage: String? = null,
@SerialName("fulfillment_message_i18n")
override val fulfillmentMessageI18n: Map<String, String>? = null,
override val products: List<ContractProduct>? = null,
@SerialName("refund_deadline")
override val refundDeadline: Timestamp? = null,
@SerialName("pay_deadline")
override val payDeadline: Timestamp? = null,
@SerialName("wire_transfer_deadline")
override val wireTransferDeadline: Timestamp? = null,
@SerialName("merchant_base_url")
override val merchantBaseUrl: String,
override val merchant: Merchant,
override val exchanges: List<Exchange> = listOf(),
@SerialName("delivery_location")
override val deliveryLocation: Location? = null,
@SerialName("delivery_date")
override val deliveryDate: Timestamp? = null,
val choices: List<ContractChoice>,
@SerialName("token_families")
val tokenFamilies: Map<String, ContractTokenFamily>,
) : ContractTerms() {
override val version: ContractVersion = ContractVersion.V1
}
}
@Serializable
data class Merchant(
val name: String,
val email: String? = null,
val website: String? = null,
val logo: String? = null,
val address: Location? = null,
val jurisdiction: Location? = null
)
@Serializable
data class Location(
val country: String? = null,
@SerialName("country_subdivision")
val countrySubdivision: String? = null,
val district: String? = null,
val town: String? = null,
@SerialName("town_location")
val townLocation: String? = null,
@SerialName("post_code")
val postCode: String? = null,
val street: String? = null,
@SerialName("building_name")
val buildingName: String? = null,
@SerialName("building_number")
val buildingNumber: String? = null,
@SerialName("address_lines")
val addressLines: List<String>? = null,
)
@Serializable
data class ContractProduct(
@SerialName("product_id")
override val productId: String? = null,
@SerialName("product_name")
override val productName: String? = null,
override val description: String,
@SerialName("description_i18n")
override val descriptionI18n: Map<String, String>? = null,
override val price: Amount? = null,
@SerialName("prices_are_net")
@EncodeDefault
val pricesAreNet: Boolean = false,
@SerialName("delivery_location")
override val location: String? = null,
override val image: String? = null,
override val taxes: Set<Tax>? = null,
val quantity: Int = 1,
) : OrderProduct() {
val totalPrice: Amount? by lazy {
price?.let { price * quantity }
}
}
@Serializable
data class Exchange(
val url: String,
)
@Serializable
data class Tax(
val name: String,
val tax: Amount,
)
@Serializable
data class ContractChoice(
val amount: Amount,
val description: String? = null,
@SerialName("description_i18n")
val descriptionI18n: Map<String, String>? = null,
val inputs: List<ContractInput>,
val outputs: List<ContractOutput>,
@SerialName("max_fee")
val maxFee: Amount,
)
@Serializable
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("type")
sealed class ContractInput {
@Serializable
@SerialName("token")
data class Token(
@SerialName("token_family_slug")
val tokenFamilySlug: String,
val count: Int = 1,
): ContractInput()
}
@Serializable
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("type")
sealed class ContractOutput {
@Serializable
@SerialName("token")
data class Token(
@SerialName("token_family_slug")
val tokenFamilySlug: String,
val count: Int = 1,
): ContractOutput()
@Serializable
@SerialName("tax-receipt")
data class TaxReceipt(
val amount: Amount,
@SerialName("donau_urls")
val donauUrls: List<String>,
): ContractOutput()
}
@Serializable
data class ContractTokenFamily(
val name: String,
val description: String,
val descriptionI18n: Map<String, String>? = null,
val details: ContractTokenDetails,
val critical: Boolean,
)
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("class")
sealed class ContractTokenDetails {
@Serializable
@SerialName("subscription")
data object Subscription: ContractTokenDetails()
@Serializable
@SerialName("discount")
data object Discount: ContractTokenDetails()
}
object ContractTermsSerializer : JsonContentPolymorphicSerializer<ContractTerms>(ContractTerms::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<ContractTerms> {
return when(val type = element.jsonObject["version"]?.jsonPrimitive?.intOrNull) {
null, 0 -> ContractTerms.V0.serializer()
1 -> ContractTerms.V1.serializer()
else -> error("unknown contract version $type")
}
}
}
/**
* Attaches a currency specification to all amounts contained in these terms.
* The resolver is called with each amount's own currency, so multi-currency
* V1 choices are handled correctly.
*/
fun ContractTerms.withSpec(
specForCurrency: (currency: String) -> CurrencySpecification?,
): ContractTerms = when (this) {
is ContractTerms.V0 -> copy(
amount = amount.withSpec(specForCurrency(amount.currency)),
maxFee = maxFee.withSpec(specForCurrency(maxFee.currency)),
products = products?.map { it.withSpec(specForCurrency) },
)
is ContractTerms.V1 -> copy(
choices = choices.map { it.withSpec(specForCurrency) },
products = products?.map { it.withSpec(specForCurrency) },
)
}
private fun ContractProduct.withSpec(
specForCurrency: (currency: String) -> CurrencySpecification?,
) = copy(
price = price?.let { it.withSpec(specForCurrency(it.currency)) },
taxes = taxes?.map {
it.copy(tax = it.tax.withSpec(specForCurrency(it.tax.currency)))
}?.toSet(),
)
private fun ContractChoice.withSpec(
specForCurrency: (currency: String) -> CurrencySpecification?,
) = copy(
amount = amount.withSpec(specForCurrency(amount.currency)),
maxFee = maxFee.withSpec(specForCurrency(maxFee.currency)),
outputs = outputs.map { output ->
if (output is ContractOutput.TaxReceipt) {
output.copy(
amount = output.amount.withSpec(
specForCurrency(output.amount.currency),
),
)
} else {
output
}
},
)