Files
job-search/internal/findATender/typeConvert.go
T

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
}