Files
job-search/internal/eusupply/convert.go
T
bwilliams 50ee510520 Start and end dates are now the contract's, with a new closing date
Find a Tender never sends tender.tenderPeriod.startDate, so an
opportunity's start date was always empty, and its end date was the
bid deadline. Start and end are now the contract period: from the
contracts' periods, else the awards' contract periods, else the lots'
contract periods, each date on its own, taking the earliest start and
the latest end of several.

The bid deadline (tenderPeriod.endDate) moves to a new closing_date
column (migration 00009), in the API as closing_date. EU Supply's
pages give no contract period, so its start and end are left empty
and its response deadline is the closing date.
2026-10-21 14:31:12 +01:00

101 lines
2.9 KiB
Go

package eusupply
import (
"encoding/json"
"strings"
"git.avsdev.uk/Go/job-search/internal/types"
)
// The opportunities Convert makes.
const (
// UUIDPrefix starts the UUID of every opportunity from the UK portal;
// the tender's id follows it.
UUIDPrefix = "eusupply-uk-"
// uuidType is their UUIDType.
uuidType = "eusupply"
// releaseTag is their one release's tag: the portal lists open tenders.
releaseTag = "tender"
)
// mainCategories maps the portal's types of contract to OCDS's
// mainProcurementCategory, as Find a Tender's opportunities have.
var mainCategories = map[string]string{
"Supplies": "goods",
"Services": "services",
"Works": "works",
}
// UUID returns the UUID of the opportunity for the tender with id.
func UUID(id string) string {
return UUIDPrefix + id
}
// ReleaseID returns the id of the one release of the opportunity for
// tender: its id, as the portal gives each tender one page.
func ReleaseID(tender Tender) string {
return tender.ID
}
// Convert returns the opportunity for each tender, with one release whose
// JSON is the tender as read.
func Convert(tenders []Tender) []*types.Opportunity {
opportunities := make([]*types.Opportunity, 0, len(tenders))
for _, tender := range tenders {
opportunities = append(opportunities, convertOne(tender))
}
return opportunities
}
// convertOne builds the opportunity for one tender. The portal's public
// pages give no contract period, so StartDate and EndDate are left nil.
func convertOne(tender Tender) *types.Opportunity {
opportunity := &types.Opportunity{
UUID: UUID(tender.ID),
UUIDType: uuidType,
Title: tender.Title,
Status: "active",
Description: tender.Description,
BuyerName: tender.Buyer,
Classifications: classifications(tender),
MainCategory: mainCategories[tender.ContractType],
MultiLot: tender.Lots > 1,
ClosingDate: tender.Deadline,
Source: Source,
LatestReleaseDate: tender.Published,
}
release := types.OpportunityRelease{
UUID: opportunity.UUID,
ReleaseID: ReleaseID(tender),
ReleaseDate: tender.Published,
Tags: []types.OpportunityReleaseTag{
{
Tag: releaseTag,
},
},
}
// A Tender is plain data, so marshalling it can't fail.
if encoded, err := json.Marshal(tender); err == nil {
release.JSON = string(encoded)
}
opportunity.Releases = []types.OpportunityRelease{release}
return opportunity
}
// classifications returns the tender's CPV codes as Find a Tender's are
// stored: without the check digit ("48000000", not "48000000-8").
func classifications(tender Tender) []types.ClassificationCode {
codes := make([]types.ClassificationCode, 0, len(tender.CPVCodes))
for _, cpv := range tender.CPVCodes {
code, _, _ := strings.Cut(cpv.Code, "-")
codes = append(codes, types.ClassificationCode{
IsCPV: true,
Code: code,
Classification: cpv.Description,
})
}
return codes
}