clavitor/clavitor.ai/admin/price_utils.go

257 lines
7.0 KiB
Go

// Price formatting and "pretty commercial price" conversion
// Design principle: Never show exact converted prices (e.g., $29.99 → €27.42)
// Instead: Round UP to psychologically pleasing local prices
package main
import (
"fmt"
"math"
"strings"
)
// CurrencyInfo holds currency metadata for formatting
type CurrencyInfo struct {
Code string
Name string
Decimals int
Symbol string
SymbolPosition string
PrettyPattern string
ExchangeRate float64
}
// FormatPrice displays an amount in minor units as human-readable string
// e.g., 149900 cents USD → "USD 1,499.00" or "$1,499.00" depending on useSymbol
func FormatPrice(amountMinor int64, currencyCode string, useSymbol bool) string {
// This is a stub - in production, fetch from DB
info := getCurrencyInfo(currencyCode)
// Convert to major units (e.g., dollars from cents)
divisor := math.Pow(10, float64(info.Decimals))
major := float64(amountMinor) / divisor
// Format with thousands separator and correct decimals
formatted := formatNumber(major, info.Decimals)
if useSymbol && info.Symbol != "" {
if info.SymbolPosition == "suffix" {
return formatted + " " + info.Symbol
}
return info.Symbol + formatted
}
return currencyCode + " " + formatted
}
// PrettyPrice converts USD amount to local currency and rounds to "pretty" commercial price
// Uses roundToNine algorithm (ends in 9, step-based rounding)
// Returns amount in MINOR units (e.g., 11900 for $119.00)
func PrettyPrice(usdAmount float64, targetCurrency string, dbRate float64) int64 {
info := getCurrencyInfo(targetCurrency)
var majorAmount float64
if targetCurrency == "USD" {
majorAmount = usdAmount
} else {
// Convert to target currency major units
majorAmount = usdAmount * dbRate
}
// Round to nice "ends in 9" price
roundedMajor := roundToNine(majorAmount)
// Convert to minor units
multiplier := math.Pow(10, float64(info.Decimals))
return int64(roundedMajor * multiplier)
}
// roundToTen rounds UP to the next clean step boundary
// roundToTen rounds UP to the next clean step boundary
// Step sizes: 10 (for 100-200), 25 (for 200-250), 50 (for 250-500), 100 (for 500-1000), etc.
func roundToTen(price float64) float64 {
if price <= 0 {
return 0
}
// Find magnitude M where 10^M <= price < 10^(M+1)
M := math.Floor(math.Log10(price))
magnitude := math.Pow(10, M)
// Normalize to 1-10 range
N := price / magnitude
// Lookup step ratio based on coefficient
var stepRatio float64
switch {
case N < 2.0:
stepRatio = 0.1 // step = 10, 100, 1000...
case N < 2.5:
stepRatio = 0.25 // step = 25, 250...
case N < 5.0:
stepRatio = 0.5 // step = 50, 500...
default:
stepRatio = 1.0 // step = 100, 1000...
}
step := stepRatio * magnitude
return math.Ceil(price/step) * step
}
// roundToNine: Commercial pricing (always ends just below the step)
func roundToNine(price float64) float64 {
return roundToTen(price) - 1
}
// formatNumber formats with thousands separators and correct decimals
func formatNumber(num float64, decimals int) string {
// Round to specified decimals first
multiplier := math.Pow(10, float64(decimals))
rounded := math.Round(num*multiplier) / multiplier
// Format with decimals
format := fmt.Sprintf("%%.%df", decimals)
withDecimals := fmt.Sprintf(format, math.Abs(rounded))
// Split integer and decimal parts
parts := strings.Split(withDecimals, ".")
intPart := parts[0]
decPart := ""
if len(parts) > 1 {
decPart = parts[1]
}
// Add thousands separators
var result strings.Builder
for i, c := range intPart {
if i > 0 && (len(intPart)-i)%3 == 0 {
result.WriteRune(',')
}
result.WriteRune(c)
}
if decimals > 0 && decPart != "" {
result.WriteString(".")
result.WriteString(decPart)
}
if rounded < 0 {
return "-" + result.String()
}
return result.String()
}
// getCurrencyInfo returns currency metadata
// In production, this queries the database
func getCurrencyInfo(code string) CurrencyInfo {
// Default fallback
info := CurrencyInfo{
Code: code,
Name: code,
Decimals: 2,
Symbol: code,
SymbolPosition: "prefix",
PrettyPattern: "x9",
ExchangeRate: 1.0,
}
// Common currencies
switch code {
case "USD":
info = CurrencyInfo{
Code: "USD", Name: "US Dollar", Decimals: 2,
Symbol: "$", SymbolPosition: "prefix",
PrettyPattern: "x9", ExchangeRate: 1.0,
}
case "EUR":
info = CurrencyInfo{
Code: "EUR", Name: "Euro", Decimals: 2,
Symbol: "€", SymbolPosition: "prefix",
PrettyPattern: "x9", ExchangeRate: 0.92,
}
case "GBP":
info = CurrencyInfo{
Code: "GBP", Name: "British Pound", Decimals: 2,
Symbol: "£", SymbolPosition: "prefix",
PrettyPattern: "x9", ExchangeRate: 0.79,
}
case "JPY":
info = CurrencyInfo{
Code: "JPY", Name: "Japanese Yen", Decimals: 0,
Symbol: "¥", SymbolPosition: "prefix",
PrettyPattern: "x0", ExchangeRate: 151.5,
}
case "CAD":
info = CurrencyInfo{
Code: "CAD", Name: "Canadian Dollar", Decimals: 2,
Symbol: "C$", SymbolPosition: "prefix",
PrettyPattern: "x9", ExchangeRate: 1.36,
}
case "AUD":
info = CurrencyInfo{
Code: "AUD", Name: "Australian Dollar", Decimals: 2,
Symbol: "A$", SymbolPosition: "prefix",
PrettyPattern: "x9", ExchangeRate: 1.52,
}
case "CHF":
info = CurrencyInfo{
Code: "CHF", Name: "Swiss Franc", Decimals: 2,
Symbol: "Fr", SymbolPosition: "prefix",
PrettyPattern: "x0", ExchangeRate: 0.90,
}
case "SEK", "NOK", "DKK":
symbol := "kr"
info = CurrencyInfo{
Code: code, Name: "Krona", Decimals: 2,
Symbol: symbol, SymbolPosition: "suffix",
PrettyPattern: "x0", ExchangeRate: 10.5,
}
case "BHD":
info = CurrencyInfo{
Code: "BHD", Name: "Bahraini Dinar", Decimals: 3,
Symbol: "BD", SymbolPosition: "prefix",
PrettyPattern: "x000", ExchangeRate: 0.376,
}
case "KWD":
info = CurrencyInfo{
Code: "KWD", Name: "Kuwaiti Dinar", Decimals: 3,
Symbol: "KD", SymbolPosition: "prefix",
PrettyPattern: "x000", ExchangeRate: 0.307,
}
}
return info
}
// ExamplePrettyPrices shows examples of pretty pricing conversion
func ExamplePrettyPrices() {
fmt.Println("=== Pretty Commercial Prices ===")
fmt.Println("(Never show exact converted prices - always round UP)")
fmt.Println()
examples := []struct {
USD float64
Rate float64
Target string
}{
{23.21, 0.92, "EUR"}, // Should be €24.00, not €21.35
{47.32, 0.92, "EUR"}, // Should be €49.99, not €43.53
{123.45, 0.92, "EUR"}, // Should be €129.00, not €113.57
{499.00, 0.92, "EUR"}, // Should be €499.00 or €459.00
{999.00, 0.79, "GBP"}, // Should be £799.00, not £789.21
{29.99, 151.5, "JPY"}, // Should be ¥5,000, not ¥4,544
{1499.00, 1.0, "USD"}, // Should be $1,499.00
{1499.00, 0.92, "EUR"}, // Should be €1,499.00, not €1,379
}
for _, ex := range examples {
pretty := PrettyPrice(ex.USD, ex.Target, ex.Rate)
exactConverted := ex.USD * ex.Rate
display := FormatPrice(pretty, ex.Target, true)
fmt.Printf("$%.2f USD @ %.4f → EXACT: %.2f → PRETTY: %s\n",
ex.USD, ex.Rate, exactConverted, display)
}
}