Find a Tender API client updates
- Convert OCDS releases to types.Opportunity (internal/types); FetchOnce and FetchAll now return opportunities, built from each ocid's latest release - Include each item's main classification as well as its additional ones - Return ErrUnexpectedStatus for non-200 responses and ErrRateLimitExceeded for a repeated 429; close discarded bodies - Stop fetchAll on a repeated next link and close each page's body once read rather than deferring in the loop - Default a zero Since to 24 hours ago - Merge date repair and releaseIDs into api.go - Filter --planning/--opportunities/--awards on each procedure's current stage, using OCDS release tags; --opportunities previously matched a nonexistent "opportunity" tag and returned nothing - Reject conflicting filters before fetching (cobra flag group)
This commit is contained in:
1 parent
69620e5f7d
commit
399eccf2de
8 files changed
+567
-86
No files matched your search
@@ -0,0 +1,190 @@
|
||||
package findATender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/findATender/ocds"
|
||||
"git.avsdev.uk/Go/job-search/internal/types"
|
||||
)
|
||||
|
||||
func ocdsGetBuyerId(release *ocds.Release) string {
|
||||
if release.Buyer != nil {
|
||||
return string(release.Buyer.ID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func ocdsGetBuyerName(release *ocds.Release) string {
|
||||
if release.Buyer != nil {
|
||||
return release.Buyer.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ocdsConvertClassification(
|
||||
classif *ocds.Classification,
|
||||
) types.ClassificationCode {
|
||||
return types.ClassificationCode{
|
||||
IsCPV: strings.Compare(classif.Scheme, "CPV") == 0,
|
||||
Code: string(classif.ID),
|
||||
Classification: classif.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func ocdsGetClassifications(release *ocds.Release) []types.ClassificationCode {
|
||||
classifications := make([]types.ClassificationCode, 0)
|
||||
if release.Tender == nil {
|
||||
return classifications
|
||||
}
|
||||
add := func(classif *ocds.Classification) {
|
||||
newClassif := ocdsConvertClassification(classif)
|
||||
found := slices.ContainsFunc(
|
||||
classifications, func(r types.ClassificationCode) bool {
|
||||
return r.Code == newClassif.Code
|
||||
},
|
||||
)
|
||||
if !found {
|
||||
classifications = append(classifications, newClassif)
|
||||
}
|
||||
}
|
||||
if release.Tender.Classification != nil {
|
||||
add(release.Tender.Classification)
|
||||
}
|
||||
for _, val := range release.Tender.Items {
|
||||
if val.Classification != nil {
|
||||
add(val.Classification)
|
||||
}
|
||||
for _, classif := range val.AdditionalClassifications {
|
||||
add(&classif)
|
||||
}
|
||||
}
|
||||
return classifications
|
||||
}
|
||||
|
||||
func ocdsIsSMESuitable(release *ocds.Release) bool {
|
||||
if release.Tender == nil {
|
||||
return false
|
||||
}
|
||||
if len(release.Tender.Lots) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, lot := range release.Tender.Lots {
|
||||
if lot.Suitability != nil {
|
||||
if lot.Suitability.SME != nil && *lot.Suitability.SME {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func ocdsIsVCSESuitable(release *ocds.Release) bool {
|
||||
if release.Tender == nil {
|
||||
return false
|
||||
}
|
||||
if len(release.Tender.Lots) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, lot := range release.Tender.Lots {
|
||||
if lot.Suitability != nil {
|
||||
if lot.Suitability.Vcse != nil && *lot.Suitability.Vcse {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ocdsRelease(release *ocds.Release) types.OpportunityRelease {
|
||||
tags := make([]types.OpportunityReleaseTag, 0)
|
||||
for _, val := range release.Tag {
|
||||
tags = append(tags, types.OpportunityReleaseTag{
|
||||
UUID: release.OCID,
|
||||
ReleaseID: release.ID,
|
||||
Tag: val,
|
||||
})
|
||||
}
|
||||
|
||||
rel := types.OpportunityRelease{
|
||||
UUID: release.OCID,
|
||||
ReleaseID: release.ID,
|
||||
Tags: tags,
|
||||
ReleaseDate: release.Date,
|
||||
}
|
||||
|
||||
reencoded, err := json.Marshal(release)
|
||||
if err == nil {
|
||||
rel.JSON = string(reencoded)
|
||||
}
|
||||
|
||||
return rel
|
||||
}
|
||||
|
||||
func convertOne(release *ocds.Release) *types.Opportunity {
|
||||
t := &types.Opportunity{
|
||||
UUID: release.OCID,
|
||||
UUIDType: "ocid",
|
||||
BuyerId: ocdsGetBuyerId(release),
|
||||
BuyerName: ocdsGetBuyerName(release),
|
||||
}
|
||||
|
||||
if release.Tender != nil {
|
||||
t.Title = release.Tender.Title
|
||||
t.Status = release.Tender.Status
|
||||
t.Description = release.Tender.Description
|
||||
t.Classifications = ocdsGetClassifications(release)
|
||||
t.MainCategory = release.Tender.MainProcurementCategory
|
||||
t.ProcurementMethod = release.Tender.ProcurementMethod
|
||||
t.SMESuitable = ocdsIsSMESuitable(release)
|
||||
t.VCSESuitable = ocdsIsVCSESuitable(release)
|
||||
if release.Tender.TenderPeriod != nil {
|
||||
t.StartDate = release.Tender.TenderPeriod.StartDate
|
||||
t.EndDate = release.Tender.TenderPeriod.EndDate
|
||||
}
|
||||
if release.Tender.Value != nil && release.Tender.Value.Amount != nil {
|
||||
t.GrossValue = *release.Tender.Value.Amount
|
||||
}
|
||||
t.HasDocuments = len(release.Tender.Documents) > 0
|
||||
t.MultiLot = len(release.Tender.Lots) > 1
|
||||
}
|
||||
|
||||
t.LatestReleaseDate = release.Date
|
||||
t.Releases = append(t.Releases, ocdsRelease(release))
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Convert groups releases by ocid into opportunities, in the order each ocid
|
||||
// first appears. Each opportunity is built from its latest release (by date,
|
||||
// or the later one in releases on a tie) and lists all of its releases oldest
|
||||
// first.
|
||||
func Convert(releases []ocds.Release) []*types.Opportunity {
|
||||
var order []string
|
||||
byOCID := make(map[string][]*ocds.Release)
|
||||
|
||||
for idx := range releases {
|
||||
release := &releases[idx]
|
||||
if _, found := byOCID[release.OCID]; !found {
|
||||
order = append(order, release.OCID)
|
||||
}
|
||||
byOCID[release.OCID] = append(byOCID[release.OCID], release)
|
||||
}
|
||||
|
||||
opportunities := make([]*types.Opportunity, 0, len(order))
|
||||
for _, ocid := range order {
|
||||
group := byOCID[ocid]
|
||||
|
||||
slices.SortStableFunc(group, func(a, b *ocds.Release) int {
|
||||
return a.Date.Compare(b.Date)
|
||||
})
|
||||
|
||||
opportunity := convertOne(group[len(group)-1])
|
||||
opportunity.Releases = make([]types.OpportunityRelease, 0, len(group))
|
||||
for _, release := range group {
|
||||
opportunity.Releases = append(opportunity.Releases, ocdsRelease(release))
|
||||
}
|
||||
opportunities = append(opportunities, opportunity)
|
||||
}
|
||||
|
||||
return opportunities
|
||||
}
|
||||
Reference in new issue
Block a user