209 lines
5.8 KiB
Go
209 lines
5.8 KiB
Go
package findATender
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/findATender/ocds"
|
|
"git.avsdev.uk/Go/job-search/internal/types"
|
|
)
|
|
|
|
// ocdsGetBuyerId returns the buyer's id, or "" if the release has no buyer.
|
|
func ocdsGetBuyerId(release *ocds.Release) string {
|
|
if release.Buyer != nil {
|
|
return string(release.Buyer.ID)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ocdsGetBuyerName returns the buyer's name, or "" if the release has no
|
|
// buyer.
|
|
func ocdsGetBuyerName(release *ocds.Release) string {
|
|
if release.Buyer != nil {
|
|
return release.Buyer.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ocdsConvertClassification converts an OCDS classification, noting whether
|
|
// it is a CPV code.
|
|
func ocdsConvertClassification(
|
|
classif *ocds.Classification,
|
|
) types.ClassificationCode {
|
|
return types.ClassificationCode{
|
|
IsCPV: strings.Compare(classif.Scheme, "CPV") == 0,
|
|
Code: string(classif.ID),
|
|
Classification: classif.Description,
|
|
}
|
|
}
|
|
|
|
// ocdsGetClassifications returns the tender's classification, then each
|
|
// item's main and additional classifications, without repeating a code.
|
|
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
|
|
}
|
|
|
|
// ocdsIsSMESuitable reports whether any lot of the tender is marked as
|
|
// suitable for SMEs. A tender with no lots is not.
|
|
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
|
|
}
|
|
|
|
// ocdsIsVCSESuitable reports whether any lot of the tender is marked as
|
|
// suitable for VCSEs (voluntary, community and social enterprises). A tender
|
|
// with no lots is not.
|
|
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
|
|
}
|
|
|
|
// ocdsRelease converts a release's identity, tags and date, and keeps a
|
|
// re-encoded copy of the whole release as JSON.
|
|
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
|
|
}
|
|
|
|
// convertOne builds an opportunity from a single release. Convert passes it
|
|
// each ocid's latest release, as that has the current state of the tender.
|
|
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
|
|
}
|