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.
101 lines
2.9 KiB
Go
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
|
|
}
|