CheckoutBackend.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.merchantpos.checkout

import net.taler.merchantlib.CheckPaymentResponse
import net.taler.merchantlib.ExternalRefundRequest
import net.taler.merchantlib.MerchantApi
import net.taler.merchantlib.MerchantConfig
import net.taler.merchantlib.PostOrderRequest

sealed interface BackendResult<out T>
{
    data class Success<T>(
        val value: T,
    ) : BackendResult<T>

    data class Failure(
        val message: String,
    ) : BackendResult<Nothing>
}

interface CheckoutBackend
{
    /** Creates the order and returns its backend ID. */
    suspend fun createOrder(request: PostOrderRequest): BackendResult<String>

    /** True when paid, false when it exists unpaid, null when unknown or unreachable. */
    suspend fun isPaid(orderId: String): Boolean?

    /** Completes an order paid entirely outside Taler, without a wallet payment. */
    suspend fun collect(orderId: String): BackendResult<Unit>

    /** Records money given back outside Taler on the order. */
    suspend fun recordExternalRefund(
        orderId: String,
        request: ExternalRefundRequest,
    ): BackendResult<Unit>

    /** Deletes the order; [force] is required when it already records external payments. */
    suspend fun deleteOrder(
        orderId: String,
        force: Boolean = false,
    ): BackendResult<Unit>
}

internal class MerchantCheckoutBackend(
    private val api: MerchantApi,
    private val config: MerchantConfig,
) : CheckoutBackend
{
    override suspend fun createOrder(request: PostOrderRequest): BackendResult<String>
    {
        var result: BackendResult<String> = noResponse
        api.postOrder(config, request).handle({ result = BackendResult.Failure(it) }) {
            result = BackendResult.Success(it.orderId)
        }
        return result
    }

    override suspend fun isPaid(orderId: String): Boolean?
    {
        var paid: Boolean? = null
        api.checkOrder(config, orderId).handle(null) { paid = it is CheckPaymentResponse.Paid }
        return paid
    }

    override suspend fun collect(orderId: String): BackendResult<Unit>
    {
        var result: BackendResult<Unit> = noResponse
        api.collectOrder(config, orderId).handle({ result = BackendResult.Failure(it) }) {
            result = BackendResult.Success(Unit)
        }
        return result
    }

    override suspend fun recordExternalRefund(
        orderId: String,
        request: ExternalRefundRequest,
    ): BackendResult<Unit>
    {
        var result: BackendResult<Unit> = noResponse
        api.refundExternal(config, orderId, request).handle({ result = BackendResult.Failure(it) }) {
            result = BackendResult.Success(Unit)
        }
        return result
    }

    override suspend fun deleteOrder(
        orderId: String,
        force: Boolean,
    ): BackendResult<Unit>
    {
        var result: BackendResult<Unit> = noResponse
        api.deleteOrder(config, orderId, force = force).handle({ result = BackendResult.Failure(it) }) {
            result = BackendResult.Success(Unit)
        }
        return result
    }

    private companion object
    {
        val noResponse = BackendResult.Failure("No response from the merchant backend")
    }
}