Перейти к содержанию

SafeDecimal Architecture

Архитектурный принцип

ЗАПРЕЩЕНО использовать decimal.Decimal в API response структурах и применять .Float64() костыли.

ОБЯЗАТЕЛЬНО использовать SafeDecimal для: - ✅ Все финансовые поля в API responses (json тэги) - ✅ Все денежные суммы, проценты, курсы, комиссии - ✅ Config структуры с финансовыми лимитами - ✅ Investment amounts, yields, balances

Технические преимущества

Умная JSON сериализация

type InvestmentResponse struct {
    Amount SafeDecimal `json:"amount"`
    // Малые суммы → numbers, большие → strings
}

Логика сериализации: - Малые суммы (в пределах JavaScript safe integer) → JSON number - Большие суммы (> 9,007,199,254,740,991) → JSON string

Frontend совместимость

  • TypeScript ожидает numbers - получает numbers для малых сумм
  • JavaScript безопасность - учитывает IEEE 754 ограничения
  • Точность вычислений - использует decimal.Decimal внутри
  • Устранение костылей - никаких .Float64() вызовов в коде

Использование в коде

Backend (Go)

import "github.com/aiseeq/saga/backend/shared/models"

// Создание SafeDecimal
amount := models.MustParseSafeDecimal("100.50")
balance := models.NewSafeDecimalFromFloat64(1000.0)

// Операции (возвращают SafeDecimal)
sum := amount.Add(balance)
result := amount.Multiply(models.NewSafeDecimalFromInt(2))

// API Response
type BalanceResponse struct {
    Balance SafeDecimal `json:"balance"`
    Invested SafeDecimal `json:"invested"`
    Yield SafeDecimal `json:"yield"`
}

SmartJSONEncoder

Для legacy структур с decimal.Decimal система применяет автоматическое преобразование на HTTP response уровне:

// backend/shared/http/helpers.go
func SendSuccess(w http.ResponseWriter, data interface{}) {
    // SmartJSONEncoder автоматически конвертирует decimal.Decimal → SafeDecimal
    // ТОЛЬКО для API responses, без изменения внутренних структур
}

Категорически запрещено

// ❌ decimal.Decimal в API responses
type BadResponse struct {
    Amount decimal.Decimal `json:"amount"` // НЕ сериализуется корректно!
}

// ❌ .Float64() костыли
amount := decimal.NewFromFloat(balance).Float64() // Потеря точности!

// ❌ float64 для денежных операций
var balance float64 = 1000.50 // IEEE 754 проблемы!

Правильные паттерны

// ✅ SafeDecimal везде для финансов
type InvestmentData struct {
    Amount SafeDecimal `json:"amount"`
    APY SafeDecimal `json:"apy"`
    MinInvestment SafeDecimal `json:"min_investment"`
}

// ✅ Операции с SafeDecimal
func CalculateYield(amount SafeDecimal, apy SafeDecimal) SafeDecimal {
    return amount.Multiply(apy).Divide(models.NewSafeDecimalFromInt(100))
}

// ✅ Валидация
func ValidateAmount(amount SafeDecimal) error {
    if amount.LessThanOrEqual(models.NewSafeDecimalFromInt(0)) {
        return errors.New("amount must be positive")
    }
    return nil
}

Система синхронизации Go ↔ PostgreSQL

SafeDecimal автоматически интегрируется с PostgreSQL через:

  1. Database mapping: NUMERIC тип в PostgreSQL → SafeDecimal в Go
  2. Type generation: make generate-types создает TypeScript типы
  3. API compatibility: SmartJSONEncoder обеспечивает корректную сериализацию

Пример миграции

-- PostgreSQL schema
CREATE TABLE investments (
    id SERIAL PRIMARY KEY,
    amount NUMERIC(20, 6) NOT NULL,  -- 6 decimals для финансовой точности
    apy NUMERIC(5, 2) NOT NULL        -- 2 decimals для процентов
);
// Go model
type Investment struct {
    ID int64 `json:"id"`
    Amount SafeDecimal `json:"amount" db:"amount"`
    APY SafeDecimal `json:"apy" db:"apy"`
}
// TypeScript (автогенерация)
interface Investment {
  id: number;
  amount: number | string; // Умная сериализация!
  apy: number | string;
}

Финансовая точность

IEEE 754 ограничения

JavaScript/TypeScript используют IEEE 754 double-precision (64-bit): - Safe integer range: ±9,007,199,254,740,991 - Precision: ~15-17 decimal digits - Проблемы: 0.1 + 0.2 ≠ 0.3 в JavaScript

Решение SafeDecimal

  • Arbitrary precision: decimal.Decimal библиотека (Go)
  • Smart serialization: numbers для safe integers, strings для больших значений
  • Frontend handling: TypeScript корректно обрабатывает оба типа

Тестирование

func TestSafeDecimalJSON(t *testing.T) {
    // Малая сумма → number
    small := models.NewSafeDecimalFromFloat64(100.50)
    json := small.MarshalJSON()
    assert.Equal(t, "100.50", string(json))

    // Большая сумма → string
    large := models.MustParseSafeDecimal("10000000000000000")
    json = large.MarshalJSON()
    assert.Equal(t, "\"10000000000000000\"", string(json))
}

Связанная документация

Best Practices

  1. Всегда используй SafeDecimal для любых финансовых данных
  2. Никогда не используй float64 для денег
  3. Избегай .Float64() конверсий - потеря точности
  4. Валидируй входные данные перед созданием SafeDecimal
  5. Тестируй граничные случаи (малые/большие суммы, отрицательные значения)



📋 Метаданные

Версия: 2.4.82

Обновлено: 2025-10-21

Статус: Published