132 lines
3 KiB
Go
132 lines
3 KiB
Go
|
|
package domain
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/shopspring/decimal"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Domain-specific errors
|
||
|
|
var (
|
||
|
|
ErrInvalidCurrency = errors.New("invalid currency")
|
||
|
|
ErrInvalidAmount = errors.New("invalid amount")
|
||
|
|
ErrNegativeAmount = errors.New("amount cannot be negative")
|
||
|
|
ErrInvalidRate = errors.New("invalid exchange rate")
|
||
|
|
ErrCurrencyMismatch = errors.New("currency mismatch")
|
||
|
|
ErrRateNotFound = errors.New("exchange rate not found")
|
||
|
|
ErrUnsupportedCurrency = errors.New("unsupported currency")
|
||
|
|
)
|
||
|
|
|
||
|
|
// Currency represents a currency with its metadata
|
||
|
|
type Currency struct {
|
||
|
|
Code string
|
||
|
|
Name string
|
||
|
|
Precision int
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewCurrency creates a new Currency with validation
|
||
|
|
func NewCurrency(code, name string, precision int) (Currency, error) {
|
||
|
|
if strings.TrimSpace(code) == "" {
|
||
|
|
return Currency{}, ErrInvalidCurrency
|
||
|
|
}
|
||
|
|
|
||
|
|
if precision < 0 {
|
||
|
|
return Currency{}, ErrInvalidCurrency
|
||
|
|
}
|
||
|
|
|
||
|
|
return Currency{
|
||
|
|
Code: strings.ToUpper(strings.TrimSpace(code)),
|
||
|
|
Name: strings.TrimSpace(name),
|
||
|
|
Precision: precision,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// String returns the currency code
|
||
|
|
func (c Currency) String() string {
|
||
|
|
return c.Code
|
||
|
|
}
|
||
|
|
|
||
|
|
// Money represents an amount in a specific currency
|
||
|
|
type Money struct {
|
||
|
|
Amount decimal.Decimal
|
||
|
|
Currency Currency
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewMoney creates a new Money instance from string amount
|
||
|
|
func NewMoney(amount string, currency Currency) (Money, error) {
|
||
|
|
if strings.TrimSpace(amount) == "" {
|
||
|
|
return Money{}, ErrInvalidAmount
|
||
|
|
}
|
||
|
|
|
||
|
|
decimalAmount, err := decimal.NewFromString(strings.TrimSpace(amount))
|
||
|
|
if err != nil {
|
||
|
|
return Money{}, ErrInvalidAmount
|
||
|
|
}
|
||
|
|
|
||
|
|
if decimalAmount.IsNegative() {
|
||
|
|
return Money{}, ErrNegativeAmount
|
||
|
|
}
|
||
|
|
|
||
|
|
return Money{
|
||
|
|
Amount: decimalAmount,
|
||
|
|
Currency: currency,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewMoneyFromDecimal creates a new Money instance from decimal.Decimal
|
||
|
|
func NewMoneyFromDecimal(amount decimal.Decimal, currency Currency) (Money, error) {
|
||
|
|
if amount.IsNegative() {
|
||
|
|
return Money{}, ErrNegativeAmount
|
||
|
|
}
|
||
|
|
|
||
|
|
return Money{
|
||
|
|
Amount: amount,
|
||
|
|
Currency: currency,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// String returns formatted money representation
|
||
|
|
func (m Money) String() string {
|
||
|
|
return fmt.Sprintf("%s %s", m.Amount.StringFixed(int32(m.Currency.Precision)), m.Currency.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rate represents an exchange rate between two currencies
|
||
|
|
type Rate struct {
|
||
|
|
From Currency
|
||
|
|
To Currency
|
||
|
|
Value decimal.Decimal
|
||
|
|
Timestamp time.Time
|
||
|
|
Source string
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewRate creates a new Rate with validation
|
||
|
|
func NewRate(from, to Currency, value decimal.Decimal, source string) (Rate, error) {
|
||
|
|
if from.Code == to.Code {
|
||
|
|
return Rate{}, ErrInvalidRate
|
||
|
|
}
|
||
|
|
|
||
|
|
if value.IsNegative() || value.IsZero() {
|
||
|
|
return Rate{}, ErrInvalidRate
|
||
|
|
}
|
||
|
|
|
||
|
|
return Rate{
|
||
|
|
From: from,
|
||
|
|
To: to,
|
||
|
|
Value: value,
|
||
|
|
Timestamp: time.Now(),
|
||
|
|
Source: strings.TrimSpace(source),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// String returns formatted rate representation
|
||
|
|
func (r Rate) String() string {
|
||
|
|
return fmt.Sprintf("1 %s = %s %s",
|
||
|
|
r.From.Code,
|
||
|
|
r.Value.StringFixed(int32(r.To.Precision)),
|
||
|
|
r.To.Code)
|
||
|
|
}
|