ProductPresentation.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.history

import net.taler.common.Amount
import net.taler.merchantlib.MerchantUnit
import net.taler.merchantlib.OrderDetailProduct

import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
import java.util.Locale

private val decimalQuantity = Regex("[0-9]+(?:\\.[0-9]{1,8})?")

private fun OrderDetailProduct.numericQuantity(): BigDecimal?
{
    val raw = unitQuantity ?: quantity.toString()
    return raw.takeIf(decimalQuantity::matches)?.toBigDecimalOrNull()
}

internal fun OrderDetailProduct.quantityLabel(
    units: Map<String, MerchantUnit>,
    locale: Locale = Locale.getDefault(),
): String
{
    val definition = units[unit]
    val label = definition?.shortLabel(locale) ?: unit?.takeIf(String::isNotBlank)
    val count =
        numericQuantity()?.let {
            NumberFormat
                .getNumberInstance(locale)
                .apply {
                    maximumFractionDigits = definition?.precision?.coerceIn(0, 8) ?: 8
                    minimumFractionDigits = 0
                    roundingMode = RoundingMode.HALF_EVEN
                    isGroupingUsed = false
                }.format(it)
        } ?: (unitQuantity ?: quantity.toString())
    return if (label == null) count else "$count\u202f$label"
}

internal data class ProductUnitPrice(
    val amount: Amount,
    val approximate: Boolean,
)

/** Derive the historic unit price from the sold line, never from current inventory prices. */
internal fun OrderDetailProduct.unitPrice(currency: String?): ProductUnitPrice?
{
    val count = numericQuantity()?.takeIf { it.signum() > 0 } ?: return null
    val total = priceForCurrency(currency) ?: return null
    val decimalTotal = BigDecimal.valueOf(total.value).add(BigDecimal.valueOf(total.fraction.toLong(), 8))
    val perUnit = decimalTotal.divide(count, 8, RoundingMode.HALF_EVEN)
    if (perUnit.signum() < 0 ||
        perUnit > BigDecimal.valueOf(Amount.MAX_VALUE).add(BigDecimal("0.99999999")))
    {
        return null
    }
    val whole = perUnit.toLong()
    val fraction = perUnit.subtract(BigDecimal.valueOf(whole)).movePointRight(8).intValueExact()
    return ProductUnitPrice(
        Amount(total.currency, whole, fraction),
        perUnit.multiply(count).compareTo(decimalTotal) != 0,
    )
}