Orders.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.merchantlib

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
import kotlinx.serialization.json.JsonObject
import net.taler.common.Amount
import net.taler.common.ExternalPayment
import net.taler.common.Location
import net.taler.common.Merchant
import net.taler.common.RelativeTime
import net.taler.common.Timestamp

@Serializable
data class PostOrderRequest(
    @SerialName("order")
    val contractTerms: net.taler.common.Order,
    @SerialName("refund_delay")
    val refundDelay: RelativeTime? = null,
    @SerialName("payment_target")
    val paymentTarget: String? = null,
    @SerialName("session_id")
    val sessionId: String? = null,
    @SerialName("inventory_products")
    val inventoryProducts: List<MinimalInventoryProduct>? = null,
    @SerialName("lock_uuids")
    val lockUuids: List<String>? = null,
    @SerialName("create_token")
    val createToken: Boolean = true,
    @SerialName("otp_id")
    val otpId: String? = null,
)

@Serializable
data class MinimalInventoryProduct(
    @SerialName("product_id")
    val productId: String,
    val quantity: Int? = null,
    @SerialName("unit_quantity")
    val unitQuantity: String? = null,
    @SerialName("product_money_pot")
    val productMoneyPot: Int? = null,
)

/**
 * Body of `POST /private/orders/$ORDER_ID/collect`, which completes an order whose
 * Taler amount is zero without a wallet (DD 96). [choiceIndex] is only for v1 orders.
 */
@Serializable
data class CollectOrderRequest(
    @SerialName("choice_index")
    val choiceIndex: Int? = null,
)

@Serializable
data class PostOrderResponse(
    @SerialName("order_id")
    val orderId: String
)

@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonClassDiscriminator("order_status")
sealed class CheckPaymentResponse {
    abstract val paid: Boolean
    abstract val paymentDetails: OrderPaymentDetails?

    @Serializable
    @SerialName("unpaid")
    data class Unpaid(
        override val paid: Boolean = false,
        @SerialName("taler_pay_uri")
        val talerPayUri: String,
        @SerialName("already_paid_order_id")
        val alreadyPaidOrderId: String? = null,
        @SerialName("proto_contract_terms")
        override val paymentDetails: OrderPaymentDetails? = null,
    ) : CheckPaymentResponse()

    @Serializable
    @SerialName("claimed")
    data class Claimed(
        override val paid: Boolean = false,
        @SerialName("contract_terms")
        override val paymentDetails: OrderPaymentDetails? = null,
    ) : CheckPaymentResponse()

    @Serializable
    @SerialName("paid")
    data class Paid(
        override val paid: Boolean = true,
        val refunded: Boolean,
        @SerialName("refund_pending")
        val refundPending: Boolean = false,
        @SerialName("refund_amount")
        val refundAmount: Amount? = null,
        @SerialName("contract_terms")
        override val paymentDetails: OrderPaymentDetails? = null,
        @SerialName("refunds_external")
        val refundsExternal: List<ExternalRefundInfo> = emptyList(),
    ) : CheckPaymentResponse()

}

/** Common payment fields shared by v0, v1 and unclaimed contract terms. */
@Serializable
data class OrderPaymentDetails(
    @SerialName("amount_external")
    val amountExternal: List<ExternalPayment> = emptyList(),
    val summary: String? = null,
    @SerialName("summary_i18n")
    val summaryI18n: Map<String, String>? = null,
    val amount: Amount? = null,
    val timestamp: Timestamp? = null,
    val merchant: Merchant? = null,
    val products: List<OrderDetailProduct> = emptyList(),
    @SerialName("fulfillment_url")
    val fulfillmentUrl: String? = null,
    @SerialName("fulfillment_message")
    val fulfillmentMessage: String? = null,
    @SerialName("fulfillment_message_i18n")
    val fulfillmentMessageI18n: Map<String, String>? = null,
    @SerialName("public_reorder_url")
    val publicReorderUrl: String? = null,
    @SerialName("delivery_location")
    val deliveryLocation: Location? = null,
    @SerialName("delivery_date")
    val deliveryDate: Timestamp? = null,
    @SerialName("max_pickup_time")
    val maxPickupTime: Timestamp? = null,
    @SerialName("refund_deadline")
    val refundDeadline: Timestamp? = null,
    @SerialName("pay_deadline")
    val payDeadline: Timestamp? = null,
    val extra: JsonObject? = null,
)

/** Read-only ProductSold fields. Backend prices already cover the entire quantity. */
@Serializable
data class OrderDetailProduct(
    val description: String,
    @SerialName("product_name")
    val productName: String? = null,
    @SerialName("description_i18n")
    val descriptionI18n: Map<String, String>? = null,
    val quantity: Int = 1,
    @SerialName("unit_quantity")
    val unitQuantity: String? = null,
    val unit: String? = null,
    val price: Amount? = null,
    val prices: List<Amount> = emptyList(),
    @SerialName("prices_are_net")
    val pricesAreNet: Boolean = false,
    val image: String? = null,
) {
    fun priceForCurrency(currency: String?): Amount? =
        prices.firstOrNull { it.currency == currency }
            ?: price?.takeIf { currency == null || it.currency == currency }
}

@Serializable
data class ExternalRefundInfo(
    val id: String,
    val method: String,
    val amount: Amount,
    val reason: String,
    val timestamp: Timestamp,
    @SerialName("payment_id")
    val paymentId: String? = null,
)