diff --git a/api/fx/ingestor/config.yml b/api/fx/ingestor/config.yml index e45386c..f9b67dc 100644 --- a/api/fx/ingestor/config.yml +++ b/api/fx/ingestor/config.yml @@ -19,6 +19,10 @@ market: quote: "EUR" symbol: "EURUSDT" invert: true + - base: "USD" + quote: "USDT" + symbol: "USDTUSD" + invert: true - base: "UAH" quote: "USDT" symbol: "USDTUAH" diff --git a/api/fx/ingestor/internal/market/cbr/connector.go b/api/fx/ingestor/internal/market/cbr/connector.go index 1aaff8e..d7e03fa 100644 --- a/api/fx/ingestor/internal/market/cbr/connector.go +++ b/api/fx/ingestor/internal/market/cbr/connector.go @@ -412,26 +412,84 @@ func buildValuteMapping(logger *zap.Logger, items []valuteItem) (*valuteMapping, isoNum := strings.TrimSpace(item.ISONum) name := strings.TrimSpace(item.Name) engName := strings.TrimSpace(item.EngName) + nominal, err := parseNominal(item.NominalStr) if err != nil { return nil, merrors.InvalidDataType("cbr: parse directory nominal: " + err.Error()) } - if id != "" && isoChar != "" { - info := valuteInfo{ - ID: id, - ISOCharCode: isoChar, - ISONumCode: isoNum, - Name: name, - EngName: engName, - Nominal: nominal, + + if id == "" || isoChar == "" { + logger.Info("Skipping invalid currency entry", + zap.String("id", id), + zap.String("iso_char", isoChar), + zap.String("name", name), + ) + continue + } + + info := valuteInfo{ + ID: id, + ISOCharCode: isoChar, + ISONumCode: isoNum, + Name: name, + EngName: engName, + Nominal: nominal, + } + + // Handle duplicate ISO char codes (e.g. DEM with different IDs / nominals). + if existing, ok := byISO[isoChar]; ok { + // Same ISO + same ID: duplicate entry, just ignore. + if existing.ID == id { + logger.Debug("Duplicate directory entry for same ISO and ID, ignoring", + zap.String("iso_code", isoChar), + zap.String("id", id), + ) + continue } - if existing, ok := byISO[isoChar]; ok && existing.ID != id { - return nil, merrors.InvalidDataType("cbr: duplicate ISO code " + isoChar) + // Different IDs but same ISO char. + // Choose canonical entry: + // 1) Prefer nominal == 1 + // 2) Otherwise prefer smaller nominal + keepExisting := true + + if existing.Nominal != 1 && info.Nominal == 1 { + keepExisting = false + } else if existing.Nominal == 1 && info.Nominal != 1 { + keepExisting = true + } else if info.Nominal < existing.Nominal { + keepExisting = false } - if existing, ok := byID[id]; ok && existing.ISOCharCode != isoChar { - return nil, merrors.InvalidDataType("cbr: duplicate valute id " + id) + + if keepExisting { + logger.Warn("Ignoring duplicate ISO currency entry with less preferred nominal", + zap.String("iso_code", isoChar), + zap.String("existing_id", existing.ID), + zap.Int64("existing_nominal", existing.Nominal), + zap.String("new_id", info.ID), + zap.Int64("new_nominal", info.Nominal), + ) + // We keep the old one, just skip the new. + continue } + + // Replace existing mapping with the new, more canonical one. + logger.Warn("Replacing currency mapping due to more canonical nominal", + zap.String("iso_code", isoChar), + zap.String("old_id", existing.ID), + zap.Int64("old_nominal", existing.Nominal), + zap.String("new_id", info.ID), + zap.Int64("new_nominal", info.Nominal), + ) + + // Update byID: drop old ID, add new one + delete(byID, existing.ID) + byID[id] = info + + // Update ISO mapping + byISO[isoChar] = info + + // Update numeric-code index if present if isoNum != "" { if existingID, ok := byNum[isoNum]; ok && existingID != id { return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum) @@ -439,12 +497,26 @@ func buildValuteMapping(logger *zap.Logger, items []valuteItem) (*valuteMapping, byNum[isoNum] = id } - logger.Info("Installing currency code", zap.String("iso_code", isoChar), zap.String("id", id)) - byISO[isoChar] = info - byID[id] = info - } else { - logger.Info("Skipping invalid currency entry", zap.String("id", id), zap.String("iso_char", isoChar)) + continue } + + // No existing ISO entry, do normal uniqueness checks. + + if existing, ok := byID[id]; ok && existing.ISOCharCode != isoChar { + return nil, merrors.InvalidDataType("cbr: duplicate valute id " + id) + } + + if isoNum != "" { + if existingID, ok := byNum[isoNum]; ok && existingID != id { + return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum) + } + byNum[isoNum] = id + } + + logger.Info("Installing currency code", zap.String("iso_code", isoChar), zap.String("id", id), zap.Int64("nominal", nominal)) + + byISO[isoChar] = info + byID[id] = info } if len(byISO) == 0 {