Enhance tender estimated value resolution and add unit tests
- Updated the `ResolvedEstimatedValueAndCurrency` method to aggregate procurement lot values when the tender-level estimated value is not set, improving accuracy in value retrieval. - Introduced the `AggregateProcurementLotEstimatedValue` function to sum estimated values from procurement lots and return the first found currency. - Modified the `ToResponseWithLanguage` method to utilize the new estimated value resolution logic. - Added unit tests for the new functionality, ensuring correct behavior for various scenarios in the `entity_test.go` and `budget_test.go` files. This update improves the handling of estimated values in tenders, enhancing the overall reliability of the tender management system.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package ted
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCurrencyTextAmountAndCurrency(t *testing.T) {
|
||||
t.Run("chardata", func(t *testing.T) {
|
||||
amount, currency := (CurrencyText{CurrencyID: "EUR", Text: "1 234,56"}).AmountAndCurrency()
|
||||
if amount != "1 234,56" {
|
||||
t.Fatalf("unexpected amount %q", amount)
|
||||
}
|
||||
if currency != "EUR" {
|
||||
t.Fatalf("unexpected currency %q", currency)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value child", func(t *testing.T) {
|
||||
amount, currency := (CurrencyText{CurrencyID: "EUR", Value: "500000"}).AmountAndCurrency()
|
||||
if amount != "500000" {
|
||||
t.Fatalf("unexpected amount %q", amount)
|
||||
}
|
||||
if currency != "EUR" {
|
||||
t.Fatalf("unexpected currency %q", currency)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContractNoticeGetBudgetSumsAllLots(t *testing.T) {
|
||||
cn := ContractNotice{
|
||||
ProcurementProjectLots: []ProcurementProjectLot{
|
||||
{
|
||||
ProcurementProject: &ProcurementProject{
|
||||
RequestedTenderTotal: &RequestedTenderTotal{
|
||||
EstimatedOverallContractAmount: CurrencyText{CurrencyID: "EUR", Text: "100000"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ProcurementProject: &ProcurementProject{
|
||||
RequestedTenderTotal: &RequestedTenderTotal{
|
||||
EstimatedOverallContractAmount: CurrencyText{CurrencyID: "EUR", Text: "250000"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
amount, currency := cn.GetBudget()
|
||||
value, ok := ParseEuropeanAmount(amount)
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse budget amount %q", amount)
|
||||
}
|
||||
if value != 350000 {
|
||||
t.Fatalf("expected 350000, got %v", value)
|
||||
}
|
||||
if currency != "EUR" {
|
||||
t.Fatalf("expected EUR, got %q", currency)
|
||||
}
|
||||
}
|
||||
+30
-3
@@ -30,11 +30,11 @@ func mapProcurementLotFromTED(lot *ProcurementProjectLot) notice.ProcurementLot
|
||||
}
|
||||
}
|
||||
if pp.RequestedTenderTotal != nil {
|
||||
a := pp.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||
if v, ok := ParseEuropeanAmount(a.Text); ok {
|
||||
amount, currencyID := pp.RequestedTenderTotal.AmountAndCurrency()
|
||||
if v, ok := ParseEuropeanAmount(amount); ok {
|
||||
out.EstimatedValue = v
|
||||
}
|
||||
out.Currency = a.CurrencyID
|
||||
out.Currency = currencyID
|
||||
}
|
||||
if pp.PlannedPeriod != nil && pp.PlannedPeriod.DurationMeasure != nil {
|
||||
dm := pp.PlannedPeriod.DurationMeasure
|
||||
@@ -167,6 +167,7 @@ func (s *TEDScraper) mapPriorInformationNoticeToTender(pin *PriorInformationNoti
|
||||
for i := range pin.ProcurementProjectLot {
|
||||
t.ProcurementLots = append(t.ProcurementLots, mapProcurementLotFromTED(&pin.ProcurementProjectLot[i]))
|
||||
}
|
||||
backfillNoticeEstimatedValueFromLots(t)
|
||||
|
||||
if duration, unit := pin.GetDuration(); duration != "" {
|
||||
t.Duration = duration
|
||||
@@ -351,6 +352,7 @@ func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileNa
|
||||
for i := range cn.ProcurementProjectLots {
|
||||
t.ProcurementLots = append(t.ProcurementLots, mapProcurementLotFromTED(&cn.ProcurementProjectLots[i]))
|
||||
}
|
||||
backfillNoticeEstimatedValueFromLots(t)
|
||||
|
||||
// Set duration information
|
||||
if duration, unit := cn.GetDuration(); duration != "" {
|
||||
@@ -506,6 +508,7 @@ func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, so
|
||||
for i := range can.ProcurementProjectLot {
|
||||
t.ProcurementLots = append(t.ProcurementLots, mapProcurementLotFromTED(&can.ProcurementProjectLot[i]))
|
||||
}
|
||||
backfillNoticeEstimatedValueFromLots(t)
|
||||
|
||||
// Set location information from main procurement project or first lot
|
||||
var realizedAddr *Address
|
||||
@@ -663,3 +666,27 @@ func (s *TEDScraper) mapOrganization(tedOrg *Organization, role string) *notice.
|
||||
|
||||
return org
|
||||
}
|
||||
|
||||
func backfillNoticeEstimatedValueFromLots(t *notice.Notice) {
|
||||
if t == nil || t.EstimatedValue > 0 {
|
||||
return
|
||||
}
|
||||
var total float64
|
||||
var currency string
|
||||
for _, lot := range t.ProcurementLots {
|
||||
if lot.EstimatedValue <= 0 {
|
||||
continue
|
||||
}
|
||||
total += lot.EstimatedValue
|
||||
if currency == "" {
|
||||
currency = strings.TrimSpace(lot.Currency)
|
||||
}
|
||||
}
|
||||
if total <= 0 {
|
||||
return
|
||||
}
|
||||
t.EstimatedValue = total
|
||||
if strings.TrimSpace(t.Currency) == "" {
|
||||
t.Currency = currency
|
||||
}
|
||||
}
|
||||
|
||||
+57
-16
@@ -3,6 +3,7 @@ package ted
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -337,15 +338,11 @@ func (pin PriorInformationNotice) GetTenderDeadline() string {
|
||||
|
||||
// GetBudget returns the estimated contract amount and currency.
|
||||
func (pin PriorInformationNotice) GetBudget() (amount string, currency string) {
|
||||
if pin.firstLot() != nil &&
|
||||
pin.firstLot().ProcurementProject != nil &&
|
||||
pin.firstLot().ProcurementProject.RequestedTenderTotal != nil {
|
||||
contractAmount := pin.firstLot().ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||
return contractAmount.Text, contractAmount.CurrencyID
|
||||
if amount, currency = budgetFromProcurementLots(pin.ProcurementProjectLot); amount != "" {
|
||||
return amount, currency
|
||||
}
|
||||
if pin.ProcurementProject != nil && pin.ProcurementProject.RequestedTenderTotal != nil {
|
||||
a := pin.ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||
return a.Text, a.CurrencyID
|
||||
return pin.ProcurementProject.RequestedTenderTotal.AmountAndCurrency()
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -1551,12 +1548,34 @@ type ProcessJustification struct {
|
||||
type RequestedTenderTotal struct {
|
||||
XMLName xml.Name `xml:"RequestedTenderTotal"`
|
||||
EstimatedOverallContractAmount CurrencyText `xml:"EstimatedOverallContractAmount"`
|
||||
TotalAmount CurrencyText `xml:"TotalAmount,omitempty"`
|
||||
}
|
||||
|
||||
// AmountAndCurrency returns the best-effort amount text and currency from RequestedTenderTotal.
|
||||
func (r *RequestedTenderTotal) AmountAndCurrency() (amount string, currency string) {
|
||||
if r == nil {
|
||||
return "", ""
|
||||
}
|
||||
if amount, currency = r.EstimatedOverallContractAmount.AmountAndCurrency(); amount != "" {
|
||||
return amount, currency
|
||||
}
|
||||
return r.TotalAmount.AmountAndCurrency()
|
||||
}
|
||||
|
||||
// CurrencyText represents text with currency attribute
|
||||
type CurrencyText struct {
|
||||
CurrencyID string `xml:"currencyID,attr,omitempty"`
|
||||
Text string `xml:",chardata"`
|
||||
Value string `xml:"Value,omitempty"`
|
||||
}
|
||||
|
||||
// AmountAndCurrency returns the amount text and currency id from a CurrencyText node.
|
||||
func (c CurrencyText) AmountAndCurrency() (amount string, currency string) {
|
||||
amount = strings.TrimSpace(c.Text)
|
||||
if amount == "" {
|
||||
amount = strings.TrimSpace(c.Value)
|
||||
}
|
||||
return amount, strings.TrimSpace(c.CurrencyID)
|
||||
}
|
||||
|
||||
// MainCommodityClassification contains main commodity classification
|
||||
@@ -1972,21 +1991,43 @@ func (cn ContractNotice) GetTenderDeadline() string {
|
||||
}
|
||||
|
||||
// GetBudget returns the estimated contract amount and currency.
|
||||
// Lot-level BT-27 is preferred; procedure-level BT-27 is used when lots omit RequestedTenderTotal.
|
||||
// Lot-level BT-27 values are summed when present; procedure-level BT-27 is used when lots omit RequestedTenderTotal.
|
||||
func (cn ContractNotice) GetBudget() (amount string, currency string) {
|
||||
if cn.firstLot() != nil &&
|
||||
cn.firstLot().ProcurementProject != nil &&
|
||||
cn.firstLot().ProcurementProject.RequestedTenderTotal != nil {
|
||||
contractAmount := cn.firstLot().ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||
return contractAmount.Text, contractAmount.CurrencyID
|
||||
if amount, currency = budgetFromProcurementLots(cn.ProcurementProjectLots); amount != "" {
|
||||
return amount, currency
|
||||
}
|
||||
if cn.ProcurementProject != nil && cn.ProcurementProject.RequestedTenderTotal != nil {
|
||||
a := cn.ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||
return a.Text, a.CurrencyID
|
||||
return cn.ProcurementProject.RequestedTenderTotal.AmountAndCurrency()
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func budgetFromProcurementLots(lots []ProcurementProjectLot) (amount string, currency string) {
|
||||
var total float64
|
||||
for i := range lots {
|
||||
pp := lots[i].ProcurementProject
|
||||
if pp == nil || pp.RequestedTenderTotal == nil {
|
||||
continue
|
||||
}
|
||||
lotAmount, lotCurrency := pp.RequestedTenderTotal.AmountAndCurrency()
|
||||
if lotAmount == "" {
|
||||
continue
|
||||
}
|
||||
value, ok := ParseEuropeanAmount(lotAmount)
|
||||
if !ok || value <= 0 {
|
||||
continue
|
||||
}
|
||||
total += value
|
||||
if currency == "" {
|
||||
currency = lotCurrency
|
||||
}
|
||||
}
|
||||
if total <= 0 {
|
||||
return "", ""
|
||||
}
|
||||
return strconv.FormatFloat(total, 'f', -1, 64), currency
|
||||
}
|
||||
|
||||
// GetDuration returns the contract duration and unit
|
||||
func (cn ContractNotice) GetDuration() (duration string, unit string) {
|
||||
if cn.firstLot() != nil &&
|
||||
@@ -2223,7 +2264,7 @@ func (can ContractAwardNotice) GetMainClassificationDescription() string {
|
||||
func (can ContractAwardNotice) GetTotalAwardValue() (amount string, currency string) {
|
||||
noticeResult := can.GetNoticeResult()
|
||||
if noticeResult != nil && noticeResult.TotalAmount != nil {
|
||||
return noticeResult.TotalAmount.Text, noticeResult.TotalAmount.CurrencyID
|
||||
return noticeResult.TotalAmount.AmountAndCurrency()
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user