Files

107 lines
3.6 KiB
Go

package mappers
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
"github.com/hanzo-fi/payments/ee/plugins/bitstamp/client"
"github.com/hanzo-fi/payments/pkg/domain/models"
)
// ConversionMapResult — Conversion != nil → emit; Skip → ignore;
// DerivativesRow → orchestrator Errorf-skip (spot-only).
type ConversionMapResult struct {
Conversion *models.PSPConversion
Skip bool
DerivativesRow bool
}
// UserTransactionToPSPConversion detects type-36 (instant buy/sell)
// rows with exactly one negative + one positive non-zero
// known-currency amount. See MAPPINGS §4.5.
func UserTransactionToPSPConversion(currencies map[string]int, tx client.UserTransaction) (ConversionMapResult, error) {
if tx.HasDerivativesMarker() {
return ConversionMapResult{Skip: true, DerivativesRow: true}, nil
}
if !IsConversionType(tx.Type) {
return ConversionMapResult{Skip: true}, nil
}
source, destination, ok, err := ResolveTwoAssetConversion(currencies, tx.CurrencyAmounts)
if err != nil {
return ConversionMapResult{}, fmt.Errorf("resolve conversion legs for tx %d: %w", tx.ID, err)
}
if !ok {
return ConversionMapResult{Skip: true}, nil
}
createdAt, err := ParseBitstampTime(tx.Datetime)
if err != nil {
return ConversionMapResult{}, fmt.Errorf("conversion tx %d: %w", tx.ID, err)
}
raw, err := json.Marshal(tx)
if err != nil {
return ConversionMapResult{}, fmt.Errorf("marshal raw for tx %d: %w", tx.ID, err)
}
pairKey, pairRate := pickPairRate(tx.PairRates, source.Symbol, destination.Symbol)
currencyPair := pairKey
if currencyPair == "" {
// Synthesise stable currency_pair when Bitstamp omits the rate key.
currencyPair = strings.ToLower(source.Symbol) + "_" + strings.ToLower(destination.Symbol)
}
conv := &models.PSPConversion{
Reference: strconv.FormatInt(tx.ID, 10),
CreatedAt: createdAt,
SourceAsset: source.Asset,
DestinationAsset: destination.Asset,
SourceAmount: source.Amount,
DestinationAmount: destination.Amount,
Status: models.CONVERSION_STATUS_COMPLETED,
SourceAccountReference: pointer.For(source.Symbol),
DestinationAccountReference: pointer.For(destination.Symbol),
Metadata: ConversionMetadata(tx, currencyPair, pairRate),
Raw: raw,
}
// Fee is in the quote currency. Identify the quote leg from the pair
// key (e.g. "usdc_eur" → quote = "eur"). For a buy the quote is the
// source; for a sell it is the destination. Fall back to destination
// when the pair key is unavailable.
feeLeg := destination
if parts := strings.SplitN(pairKey, "_", 2); len(parts) == 2 {
if strings.EqualFold(source.Symbol, parts[1]) {
feeLeg = source
}
}
if !IsZeroAmount(tx.Fee) {
fee, ferr := ParseDecimalAmount(AbsAmount(tx.Fee), feeLeg.Precision)
if ferr != nil {
return ConversionMapResult{}, fmt.Errorf("conversion tx %d fee: %w", tx.ID, ferr)
}
conv.Fee = fee
feeAsset := feeLeg.Asset
conv.FeeAsset = &feeAsset
}
return ConversionMapResult{Conversion: conv}, nil
}
// pickPairRate tries both <src>_<dst> and <dst>_<src> — Bitstamp
// keys the rate on the market pair, not the trade direction.
func pickPairRate(pairs map[string]string, source, destination string) (key, rate string) {
src := strings.ToLower(source)
dst := strings.ToLower(destination)
for _, candidate := range []string{src + "_" + dst, dst + "_" + src} {
if v, ok := pairs[candidate]; ok && v != "" {
return candidate, v
}
}
return "", ""
}