From ee5414bc10ceacb05b8f4aeaaa5f1a506a967768 Mon Sep 17 00:00:00 2001 From: Mazyar Date: Sun, 21 Jun 2026 09:41:33 +0330 Subject: [PATCH] Enhance procurement lot estimated value aggregation and add unit tests - Updated the `AggregateProcurementLotEstimatedValue` function to reject mixed currencies and allow empty lot currencies, ensuring accurate aggregation of estimated values. - Introduced new unit tests in `entity_test.go` to validate the behavior of the aggregation function under various scenarios, including mixed currencies and empty lot currencies. - Refactored budget calculation in `budget_test.go` to utilize the new aggregation logic, improving consistency in budget retrieval. This update improves the handling of estimated values in procurement lots, enhancing the reliability of the tender management system. --- internal/tender/entity.go | 29 +++++++++++++++++++----- internal/tender/entity_test.go | 23 +++++++++++++++++++ ted/aggregate.go | 41 ++++++++++++++++++++++++++++++++++ ted/budget_test.go | 26 +++++++++++++++++++++ ted/mapper.go | 12 +++++----- ted/model.go | 9 ++++---- 6 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 ted/aggregate.go diff --git a/internal/tender/entity.go b/internal/tender/entity.go index 0f0f9f6..62cbbdf 100644 --- a/internal/tender/entity.go +++ b/internal/tender/entity.go @@ -309,18 +309,37 @@ func (t *Tender) HasEstimatedValue() bool { return value > 0 } -// AggregateProcurementLotEstimatedValue sums lot-level estimated values and returns the first lot currency found. +// AggregateProcurementLotEstimatedValue sums lot-level estimated values when all non-empty +// lot currencies match. Conflicting currencies are not aggregated. func AggregateProcurementLotEstimatedValue(lots []ProcurementLot) (value float64, currency string) { + var refCurrency string + for _, lot := range lots { + if lot.EstimatedValue <= 0 { + continue + } + c := strings.TrimSpace(lot.Currency) + if c == "" { + continue + } + if refCurrency == "" { + refCurrency = c + continue + } + if c != refCurrency { + return 0, "" + } + } + for _, lot := range lots { if lot.EstimatedValue <= 0 { continue } value += lot.EstimatedValue - if currency == "" { - currency = strings.TrimSpace(lot.Currency) - } } - return value, currency + if value <= 0 { + return 0, "" + } + return value, refCurrency } // ResolvedEstimatedValueAndCurrency returns the tender-level estimated value when set, diff --git a/internal/tender/entity_test.go b/internal/tender/entity_test.go index f9a413e..72ec624 100644 --- a/internal/tender/entity_test.go +++ b/internal/tender/entity_test.go @@ -15,6 +15,29 @@ func TestAggregateProcurementLotEstimatedValue(t *testing.T) { } } +func TestAggregateProcurementLotEstimatedValueRejectsMixedCurrencies(t *testing.T) { + value, currency := AggregateProcurementLotEstimatedValue([]ProcurementLot{ + {EstimatedValue: 100000, Currency: "EUR"}, + {EstimatedValue: 250000, Currency: "USD"}, + }) + if value != 0 || currency != "" { + t.Fatalf("expected no aggregation for mixed currencies, got value=%v currency=%q", value, currency) + } +} + +func TestAggregateProcurementLotEstimatedValueAllowsEmptyLotCurrency(t *testing.T) { + value, currency := AggregateProcurementLotEstimatedValue([]ProcurementLot{ + {EstimatedValue: 100000, Currency: "EUR"}, + {EstimatedValue: 250000}, + }) + if value != 350000 { + t.Fatalf("expected total 350000, got %v", value) + } + if currency != "EUR" { + t.Fatalf("expected currency EUR, got %q", currency) + } +} + func TestResolvedEstimatedValueAndCurrencyUsesLotsWhenTopLevelMissing(t *testing.T) { tender := &Tender{ Currency: "EUR", diff --git a/ted/aggregate.go b/ted/aggregate.go new file mode 100644 index 0000000..58a560c --- /dev/null +++ b/ted/aggregate.go @@ -0,0 +1,41 @@ +package ted + +import "strings" + +type valuedAmount struct { + value float64 + currency string +} + +// aggregateSameCurrencyAmounts sums values only when every non-empty currency matches. +// Lots with an empty currency are included; conflicting currencies yield zero. +func aggregateSameCurrencyAmounts(amounts []valuedAmount) (total float64, currency string) { + var refCurrency string + for _, item := range amounts { + if item.value <= 0 { + continue + } + c := strings.TrimSpace(item.currency) + if c == "" { + continue + } + if refCurrency == "" { + refCurrency = c + continue + } + if c != refCurrency { + return 0, "" + } + } + + for _, item := range amounts { + if item.value <= 0 { + continue + } + total += item.value + } + if total <= 0 { + return 0, "" + } + return total, refCurrency +} diff --git a/ted/budget_test.go b/ted/budget_test.go index 230f9de..6dd92be 100644 --- a/ted/budget_test.go +++ b/ted/budget_test.go @@ -56,3 +56,29 @@ func TestContractNoticeGetBudgetSumsAllLots(t *testing.T) { t.Fatalf("expected EUR, got %q", currency) } } + +func TestContractNoticeGetBudgetRejectsMixedCurrencies(t *testing.T) { + cn := ContractNotice{ + ProcurementProjectLots: []ProcurementProjectLot{ + { + ProcurementProject: &ProcurementProject{ + RequestedTenderTotal: &RequestedTenderTotal{ + EstimatedOverallContractAmount: CurrencyText{CurrencyID: "EUR", Text: "100000"}, + }, + }, + }, + { + ProcurementProject: &ProcurementProject{ + RequestedTenderTotal: &RequestedTenderTotal{ + EstimatedOverallContractAmount: CurrencyText{CurrencyID: "USD", Text: "250000"}, + }, + }, + }, + }, + } + + amount, currency := cn.GetBudget() + if amount != "" || currency != "" { + t.Fatalf("expected no budget for mixed currencies, got amount=%q currency=%q", amount, currency) + } +} diff --git a/ted/mapper.go b/ted/mapper.go index 6582b5e..33d926a 100644 --- a/ted/mapper.go +++ b/ted/mapper.go @@ -671,17 +671,17 @@ func backfillNoticeEstimatedValueFromLots(t *notice.Notice) { if t == nil || t.EstimatedValue > 0 { return } - var total float64 - var currency string + amounts := make([]valuedAmount, 0, len(t.ProcurementLots)) for _, lot := range t.ProcurementLots { if lot.EstimatedValue <= 0 { continue } - total += lot.EstimatedValue - if currency == "" { - currency = strings.TrimSpace(lot.Currency) - } + amounts = append(amounts, valuedAmount{ + value: lot.EstimatedValue, + currency: lot.Currency, + }) } + total, currency := aggregateSameCurrencyAmounts(amounts) if total <= 0 { return } diff --git a/ted/model.go b/ted/model.go index e7e5047..30687d9 100644 --- a/ted/model.go +++ b/ted/model.go @@ -2003,7 +2003,7 @@ func (cn ContractNotice) GetBudget() (amount string, currency string) { } func budgetFromProcurementLots(lots []ProcurementProjectLot) (amount string, currency string) { - var total float64 + amounts := make([]valuedAmount, 0, len(lots)) for i := range lots { pp := lots[i].ProcurementProject if pp == nil || pp.RequestedTenderTotal == nil { @@ -2017,11 +2017,10 @@ func budgetFromProcurementLots(lots []ProcurementProjectLot) (amount string, cur if !ok || value <= 0 { continue } - total += value - if currency == "" { - currency = lotCurrency - } + amounts = append(amounts, valuedAmount{value: value, currency: lotCurrency}) } + + total, currency := aggregateSameCurrencyAmounts(amounts) if total <= 0 { return "", "" }