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.
This commit is contained in:
@@ -309,18 +309,37 @@ func (t *Tender) HasEstimatedValue() bool {
|
|||||||
return value > 0
|
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) {
|
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 {
|
for _, lot := range lots {
|
||||||
if lot.EstimatedValue <= 0 {
|
if lot.EstimatedValue <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
value += lot.EstimatedValue
|
value += lot.EstimatedValue
|
||||||
if currency == "" {
|
|
||||||
currency = strings.TrimSpace(lot.Currency)
|
|
||||||
}
|
}
|
||||||
|
if value <= 0 {
|
||||||
|
return 0, ""
|
||||||
}
|
}
|
||||||
return value, currency
|
return value, refCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedEstimatedValueAndCurrency returns the tender-level estimated value when set,
|
// ResolvedEstimatedValueAndCurrency returns the tender-level estimated value when set,
|
||||||
|
|||||||
@@ -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) {
|
func TestResolvedEstimatedValueAndCurrencyUsesLotsWhenTopLevelMissing(t *testing.T) {
|
||||||
tender := &Tender{
|
tender := &Tender{
|
||||||
Currency: "EUR",
|
Currency: "EUR",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -56,3 +56,29 @@ func TestContractNoticeGetBudgetSumsAllLots(t *testing.T) {
|
|||||||
t.Fatalf("expected EUR, got %q", currency)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-6
@@ -671,17 +671,17 @@ func backfillNoticeEstimatedValueFromLots(t *notice.Notice) {
|
|||||||
if t == nil || t.EstimatedValue > 0 {
|
if t == nil || t.EstimatedValue > 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var total float64
|
amounts := make([]valuedAmount, 0, len(t.ProcurementLots))
|
||||||
var currency string
|
|
||||||
for _, lot := range t.ProcurementLots {
|
for _, lot := range t.ProcurementLots {
|
||||||
if lot.EstimatedValue <= 0 {
|
if lot.EstimatedValue <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
total += lot.EstimatedValue
|
amounts = append(amounts, valuedAmount{
|
||||||
if currency == "" {
|
value: lot.EstimatedValue,
|
||||||
currency = strings.TrimSpace(lot.Currency)
|
currency: lot.Currency,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
total, currency := aggregateSameCurrencyAmounts(amounts)
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -2003,7 +2003,7 @@ func (cn ContractNotice) GetBudget() (amount string, currency string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func budgetFromProcurementLots(lots []ProcurementProjectLot) (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 {
|
for i := range lots {
|
||||||
pp := lots[i].ProcurementProject
|
pp := lots[i].ProcurementProject
|
||||||
if pp == nil || pp.RequestedTenderTotal == nil {
|
if pp == nil || pp.RequestedTenderTotal == nil {
|
||||||
@@ -2017,11 +2017,10 @@ func budgetFromProcurementLots(lots []ProcurementProjectLot) (amount string, cur
|
|||||||
if !ok || value <= 0 {
|
if !ok || value <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
total += value
|
amounts = append(amounts, valuedAmount{value: value, currency: lotCurrency})
|
||||||
if currency == "" {
|
|
||||||
currency = lotCurrency
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
total, currency := aggregateSameCurrencyAmounts(amounts)
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user