PosConfig.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.merchantpos.config
import android.os.Build.VERSION.SDK_INT
import net.taler.common.Amount
import net.taler.common.ContractProduct
import net.taler.common.OrderProduct
import net.taler.common.TalerUtils
import net.taler.common.Tax
import net.taler.merchantlib.MerchantConfig
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.UUID
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
sealed class Config
{
abstract fun isValid(): Boolean
abstract fun hasPassword(): Boolean
abstract fun savePassword(): Boolean
/**
* Merchant URL + access token
*/
data class New(
val merchantUrl: String,
val accessToken: String,
val savePassword: Boolean,
) : Config()
{
override fun isValid() = merchantUrl.isNotBlank()
override fun hasPassword() = accessToken.isNotBlank()
override fun savePassword() = savePassword
}
}
@Serializable
data class PosConfig(
@SerialName("config")
val merchantConfig: MerchantConfig? = null,
val categories: List<Category>,
val products: List<ConfigProduct>,
)
@Serializable
data class Category(
val id: Int,
val name: String,
@SerialName("name_i18n")
val nameI18n: Map<String, String>? = null,
)
{
var selected: Boolean = false
val localizedName: String
get() = if (SDK_INT >= 26) TalerUtils.getLocalizedString(nameI18n, name) else name
}
@Serializable
data class ConfigProduct(
val id: String = UUID.randomUUID().toString(),
@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,
@SerialName("prices_are_net")
val pricesAreNet: Boolean = false,
@SerialName("delivery_location")
override val location: String? = null,
override val image: String? = null,
override val taxes: Set<Tax>? = null,
val categories: List<Int>,
val quantity: Int = 0,
@SerialName("total_stock")
val totalStock: Int? = null,
@SerialName("unit_total_stock")
val unitTotalStock: String? = null,
@SerialName("total_sold")
val totalSold: Int? = null,
@SerialName("unit_total_sold")
val unitTotalSold: String? = null,
@SerialName("total_lost")
val totalLost: Int? = null,
@SerialName("unit_total_lost")
val unitTotalLost: String? = null,
val availableToSell: Boolean = true,
val remainingStock: Int? = null,
val currencyMismatch: Boolean = false,
) : OrderProduct()
{
private val normalizedProductName: String?
get() = productName?.sanitizeVisibleText()?.takeIf { it.isNotEmpty() }
private val normalizedDescription: String
get() = localizedDescription.sanitizeVisibleText()
val totalPrice: Amount
get() = (price * quantity).withSpec(price.spec)
val displayName: String
get() = normalizedProductName ?: normalizedDescription
// Description translations only name the product when no separate name is available.
val displayNameI18n: Map<String, String>?
get() = if (normalizedProductName == null) descriptionI18n else null
val displayDescription: String?
get() =
normalizedProductName
?.takeIf { it != normalizedDescription }
?.let { normalizedDescription.takeIf(String::isNotEmpty) }
val displayPrice: String
get() = price.toString()
val stableKey: String
get() =
listOf(
productId?.trim().orEmpty(),
productName?.trim().orEmpty(),
description.trim(),
price.currency,
price.value.toString(),
price.fraction.toString(),
categories.joinToString(","),
).joinToString("|")
val stockLimit: Int?
get()
{
if (totalStock == -1 ||
unitTotalStock == "-1")
{
return null
}
val total = unitTotalStock?.toBigDecimalOrNull() ?: totalStock?.toBigDecimal() ?: return null
val sold = unitTotalSold?.toBigDecimalOrNull() ?: totalSold?.toBigDecimal() ?: BigDecimal.ZERO
val lost = unitTotalLost?.toBigDecimalOrNull() ?: totalLost?.toBigDecimal() ?: BigDecimal.ZERO
return (total - sold - lost)
.coerceIn(BigDecimal.ZERO, Int.MAX_VALUE.toBigDecimal())
.setScale(0, RoundingMode.DOWN)
.toInt()
}
fun toContractProduct() =
ContractProduct(
productId = productId,
productName = productName ?: description,
description = description,
descriptionI18n = descriptionI18n?.takeIf { it.isNotEmpty() },
price = price,
pricesAreNet = pricesAreNet,
location = null,
image = image?.takeIf { it.isNotBlank() },
taxes = taxes?.takeIf { it.isNotEmpty() },
quantity = quantity,
)
}
private fun String.sanitizeVisibleText(): String =
buildString {
var offset = 0
while (offset < this@sanitizeVisibleText.length)
{
val codePoint = Character.codePointAt(this@sanitizeVisibleText, offset)
when (Character.getType(codePoint))
{
Character.FORMAT.toInt(),
Character.CONTROL.toInt(),
Character.SURROGATE.toInt(),
Character.PRIVATE_USE.toInt(),
Character.UNASSIGNED.toInt()
-> Unit
else -> appendCodePoint(codePoint)
}
offset += Character.charCount(codePoint)
}
}.trim()