// 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) } }