Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update tenders and networks #19

Merged
merged 9 commits into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion api/serializers/bid.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const lotSerializer = require('./lot');

bidSerializer.formatBid = function (bid) {
const formattedBid = _.pick(bid, ['isWinning', 'isSubcontracted', 'xYearApproximated']);
formattedBid.TEDCANID = bid.xTEDCANID;
formattedBid.value = _.get(bid, 'price.netAmountEur') || undefined;
formattedBid.xAmountApproximated = _.get(bid, 'price.xAmountApproximated');
return formattedBid;
Expand Down
4 changes: 2 additions & 2 deletions api/serializers/network_actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const bidSerializer = require('./bid');
function formatNetworkActor(networkActor) {
const formattedActor = _.pick(
networkActor,
['id', 'label', 'type', 'medianCompetition', 'value', 'country'],
['id', 'label', 'type', 'medianCompetition', 'value'],
);
formattedActor.flags = {};
formattedActor.hidden = !networkActor.active;
return formattedActor;
}

function formatActorWithDetails(network, networkActor, nodeIDs) {
const node = _.pick(networkActor, ['label', 'id', 'type', 'medianCompetition', 'value']);
const node = _.pick(networkActor, ['label', 'id', 'type', 'medianCompetition', 'value', 'countries']);
node.flags = {};
node.hidden = !networkActor.active;
const edgeToBidClass = networkActor.type === 'buyer' ? 'Awards' : 'Participates';
Expand Down
1 change: 0 additions & 1 deletion api/serializers/tender.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ tenderSerializer.formatTender = function (tender) {
const formattedTender = _.pick(tender, ['id', 'title', 'titleEnglish', 'description', 'sources',
'isCoveredByGpa', 'isFrameworkAgreement', 'procedureType', 'year', 'country', 'isDirective', 'xYearApproximated']);
formattedTender.isEUFunded = tender.xIsEuFunded;
formattedTender.TEDCNID = tender.xTEDCNID;
formattedTender.isDirective = tender.xIsDirective;
formattedTender.finalValue = _.get(tender, 'finalPrice.netAmountEur') || undefined;
formattedTender.xAmountApproximated = _.get(tender, 'finalPrice.xAmountApproximated');
Expand Down
24 changes: 12 additions & 12 deletions api/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1346,9 +1346,12 @@ definitions:
enum:
- bidder
- buyer
country:
type: string
description: ISO 3166-1 alpha-2 country code
countries:
type: array
description: Array of bidder/buyer country codes
items:
type: string
description: ISO 3166-1 alpha-2 country code of bidder/buyer
medianCompetition:
type: number
format: double
Expand Down Expand Up @@ -1556,9 +1559,6 @@ definitions:
isWinning:
type: boolean
description: Did this bid won the lot?
TEDCANID:
type: string
description: ID of the Contract Award Notice on ted.europa.eu
isSubcontracted:
type: boolean
description: Did the winner subcontract another company for this bid?
Expand Down Expand Up @@ -1629,9 +1629,6 @@ definitions:
items:
type: string
description: URL to tender source
TEDCNID:
type: string
description: ID of the Contract Notice on ted.europa.eu
title:
type: string
description: Title of the tender
Expand Down Expand Up @@ -1744,9 +1741,12 @@ definitions:
enum:
- bidder
- buyer
country:
type: string
description: Actor country
countries:
type: array
description: Array of bidder/buyer country codes
items:
type: string
description: ISO 3166-1 alpha-2 country code of bidder/buyer
Address:
type: object
properties:
Expand Down
4 changes: 3 additions & 1 deletion api/writers/actor_cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function createCluster(networkID, clusterParams) {
label: clusterParams.label,
type: clusterParams.type,
active: true,
countries: _.uniq(calcuatedAttrs.countries),
value: calcuatedAttrs[network.settings.nodeSize],
medianCompetition: calcuatedAttrs.medianCompetition,
};
Expand Down Expand Up @@ -114,7 +115,8 @@ function calculateCluster(edgeToBidClass, network, actorIDs) {
const clusterQuery = `SELECT
count(*) as numberOfWinningBids,
sum(price.netAmountEur) as amountOfMoneyExchanged,
median(out('AppliedTo').bidsCount) as medianCompetition
median(out('AppliedTo').bidsCount) as medianCompetition,
unionall(in('${edgeToBidClass}').address.country) as countries
FROM Bid
WHERE ${_.join(networkWriters.queryToBidFilters(network.query), ' AND ')}
AND in('${edgeToBidClass}').id in :actorIDs
Expand Down
99 changes: 98 additions & 1 deletion api/writers/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,83 @@ async function createNetwork(networkParams, user) {
return transaction.commit(2).return(`$${networkName}`).one();
}

async function updateNetwork(networkParams, existingNetwork) {
const clusterWriters = require('./actor_cluster');
const newNetworkQuery = _.get(networkParams, 'query', undefined);
let networkQuery = {};

if (_.isUndefined(newNetworkQuery) === true) {
networkQuery = existingNetwork.query;
} else {
networkQuery = _.pickBy(newNetworkQuery, (val) => !(_.isUndefined(val)));
}
if (_.isEmpty(networkQuery) === true) {
throw new codes.BadRequestError('Network "query" can\'t be empty.');
}

// We have to retrieve the clusters here before we delete the actor nodes
const clusterQuery = `SELECT *,
out('Includes').in('ActingAs').id as originalActorIDs
FROM ActorCluster
WHERE out('PartOf').id=:networkID`;
const existingClusters = await config.db.query(
clusterQuery,
{ params: { networkID: existingNetwork.id } }
);

const networkName = recordName(existingNetwork.id, 'Network');
const newNetworkAttrs = Object.assign({}, {
name: _.get(networkParams, 'name', existingNetwork.name),
settings: _.get(networkParams, 'settings', existingNetwork.settings),
synopsis: _.get(networkParams, 'synopsis', existingNetwork.synopsis),
query: networkQuery,
created: existingNetwork.created,
updated: moment().format('YYYY-MM-DD HH:mm:ss'),
});

const transaction = config.db.let(networkName, (t) => {
t.update('Network')
.set(newNetworkAttrs)
.where({ '@rid': existingNetwork['@rid'] })
.return('AFTER');
});

await deleteNetworkActors(transaction, existingNetwork);

const networkActorsMapping = await Promise.join(
createBuyerNodes(transaction, newNetworkAttrs.settings, networkQuery, networkName),
createBidderNodes(transaction, newNetworkAttrs.settings, networkQuery, networkName),
(buyerActorsMapping, bidderActorsMapping) =>
Object.assign(buyerActorsMapping, bidderActorsMapping),
);
await Promise.all([
createContractsEdges(transaction, newNetworkAttrs.settings, networkQuery, networkActorsMapping),
createPartnersEdges(transaction, 'Awards', networkQuery, networkActorsMapping),
createPartnersEdges(transaction, 'Participates', networkQuery, networkActorsMapping),
]);

return transaction.commit(2).return(`$${networkName}`).one()
.then((updatedNetwork) =>
Promise.map(existingClusters, (existingCluster) => {
const clusterActorsQuery = `SELECT * FROM NetworkActor
WHERE out('PartOf').id=:networkID
AND in('ActingAs').id in :actorIDs
`
return config.db.query(
clusterActorsQuery,
{ params: { networkID: updatedNetwork.id, actorIDs: existingCluster.originalActorIDs}},
).then((newClusterActors) => {
const newNodeIDs = _.map(newClusterActors, 'id');
return clusterWriters.updateCluster(
updatedNetwork.id,
existingCluster.id,
{ nodes: newNodeIDs }
);
})
})
);
}

function queryToBidFilters(networkQuery) {
const filters = [];
const actorFilters = [];
Expand Down Expand Up @@ -91,6 +168,7 @@ function createBidderNodes(transaction, networkSettings, networkQuery, networkNa
bidder[@rid] as bidderRID,
count(*) as numberOfWinningBids,
sum(price.netAmountEur) as amountOfMoneyExchanged,
bidder.address.country as country,
median(out('AppliedTo').bidsCount) as medianCompetition
FROM (
SELECT *, in('Participates') as bidder
Expand All @@ -111,6 +189,7 @@ function createBidderNodes(transaction, networkSettings, networkQuery, networkNa
nodeAttrs.id = uuidv4();
nodeAttrs.type = 'bidder';
nodeAttrs.active = true;
nodeAttrs.countries = [node.country];
nodeAttrs.value = node[networkSettings.nodeSize];
const partnerName = createNetworkActor(transaction, nodeAttrs, node.bidderRID, networkName);
bidderActorMapping[node.bidderRID] = partnerName;
Expand Down Expand Up @@ -140,11 +219,12 @@ function createBuyerNodes(transaction, networkSettings, networkQuery, networkNam
.then((buyerNodes) => Promise.map(buyerNodes, (node) => {
const nodeAttrs = _.pick(
node,
['label', 'medianCompetition', 'country'],
['label', 'medianCompetition'],
);
nodeAttrs.id = uuidv4();
nodeAttrs.type = 'buyer';
nodeAttrs.active = true;
nodeAttrs.countries = [node.country];
nodeAttrs.value = node[networkSettings.nodeSize];
const partnerName = createNetworkActor(transaction, nodeAttrs, node.buyerRID, networkName);
buyerActorMapping[node.buyerRID] = partnerName;
Expand Down Expand Up @@ -259,6 +339,22 @@ function createNetworkEdge(transaction, edgeClass, edgeAttrs, fromName, toName)
return edgeName;
}

function deleteNetworkActors(transaction, existingNetwork) {
return config.db.select().from('NetworkActor')
.where({
"out('PartOf').id": existingNetwork.id,
"@class": 'NetworkActor',
})
.all()
.then((networkActors) => Promise.map(networkActors, (networkActor) => {
const deleteActorName = recordName(networkActor.id, 'delete');

transaction.let(deleteActorName, (t) =>
t.delete('vertex', 'NetworkActor')
.where({ '@rid': networkActor['@rid'] }));
}));
}

module.exports = {
createNetwork,
createBidderNodes,
Expand All @@ -270,4 +366,5 @@ module.exports = {
createNetworkEdge,
queryToBidFilters,
recordName,
updateNetwork,
};
90 changes: 42 additions & 48 deletions api/writers/tender.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function recordName(id, className) {

// Returns true
// Raises OrientDBError if the writing failed
async function writeTender(fullTenderRecord, skipMilitaryFilters = false) {
async function writeTender(fullTenderRecord, skipMilitaryFilters = true) {
const awardedLots = _.filter(fullTenderRecord.lots, { status: 'AWARDED' });

// If there are no awarded lots don't even process the tender
Expand Down Expand Up @@ -60,6 +60,19 @@ async function writeTender(fullTenderRecord, skipMilitaryFilters = false) {
}
});


// The tender already exists. We're deleting the existing CPV, Buyer, Bidder edges
// We also delete the existing Lots and Bids before creating new ones
if (_.isUndefined(existingTender) === false) {
await deleteHasCPV(transaction, tenderName, existingTenderID)
await deleteCreates(transaction, tenderName, existingTenderID)
const existingLotRel = await config.db.select("out('Comprises')").from('Tender')
.where({ '@rid': existingTenderID }).one();
const existingLotRIDs = existingLotRel.out;
await Promise.map(existingLotRIDs, (existingLotRID) =>
deleteLot(transaction, existingLotRID));
}

const processedCpvs = await Promise.map((fullTenderRecord.cpvs || []), (rawCpv) =>
upsertCpv(transaction, rawCpv, existingTenderID, tenderName));
// Only process further valid cpvs
Expand All @@ -71,14 +84,6 @@ async function writeTender(fullTenderRecord, skipMilitaryFilters = false) {
(rawBuyer) => upsertBuyer(transaction, rawBuyer, existingTenderID, tenderName, fullTenderRecord), // eslint-disable-line max-len
);

if (_.isUndefined(existingTender) === false) {
const existingLotRel = await config.db.select("out('Comprises')").from('Tender')
.where({ '@rid': existingTenderID }).one();
const existingLotRIDs = existingLotRel.out;
await Promise.map(existingLotRIDs, (existingLotRID) =>
deleteLot(transaction, existingLotRID));
}

await Promise.map((awardedLots || []), (rawLot) => {
const rawBids = (rawLot.bids || []);
return createLot(transaction, rawLot, tenderName, fullTenderRecord)
Expand All @@ -94,6 +99,24 @@ async function writeTender(fullTenderRecord, skipMilitaryFilters = false) {
});
}

async function deleteHasCPV(transaction, tenderName, existingTenderID) {
const operationName = `deleteHasCPV${tenderName}`;

transaction.let(operationName, (t) =>
t.delete('edge', 'HasCPV')
.where({ out: existingTenderID}));
return operationName;
}

async function deleteCreates(transaction, tenderName, existingTenderID){
const operationName = `deleteCreates${tenderName}`;

transaction.let(operationName, (t) =>
t.delete('edge', 'Creates')
.where({ in: existingTenderID}));
return operationName;
}

async function deleteLot(transaction, lotRID) {
const lotName = `delete${recordName(uuidv4(), 'Lot')}`;

Expand Down Expand Up @@ -190,24 +213,11 @@ async function upsertBuyer(transaction, rawBuyer, existingTenderID, tenderName,
});
}

const existingRel = await config.db.select().from('Creates')
.where({
in: (existingTenderID || null),
out: (existingBuyerID || null),
}).one();
transaction.let(`${buyerName}creates${tenderName}`, (t) => {
if (_.isUndefined(existingRel)) {
t.create('edge', 'Creates')
.from(`$${buyerName}`)
.to(`$${tenderName}`)
.set(buyerExtractor.extractCreates(rawBuyer));
} else {
t.update('Creates')
.set(buyerExtractor.extractCreates(rawBuyer))
.where({ '@rid': existingRel['@rid'] })
.return('AFTER');
}
});
transaction.let(`${buyerName}creates${tenderName}`, (t) =>
t.create('edge', 'Creates')
.from(`$${buyerName}`)
.to(`$${tenderName}`)
.set(buyerExtractor.extractCreates(rawBuyer)));
return buyerName;
}

Expand Down Expand Up @@ -268,29 +278,13 @@ async function upsertCpv(transaction, rawCpv, existingTenderID, tenderName) {
}
}

const existingRel = await config.db.select().from('HasCPV')
.where({
// This is needed because undefined confuses OrientDB
in: (existingCpvID || null),
out: (existingTenderID || null),
}).one();
const edgeName = `${tenderName}has${cpvName}`;
if (_.includes(_.map(transaction._state.bcommon, (arr) => arr[0]), edgeName) === false) {
if (_.isUndefined(existingRel)) {
transaction.let(edgeName, (t) => {
t.create('edge', 'HasCPV')
.from(`$${tenderName}`)
.to(`$${cpvName}`)
.set(cpvExtractor.extractHasCpv(rawCpv));
});
} else {
transaction.let(edgeName, (t) => {
t.update('HasCPV')
.set(cpvExtractor.extractHasCpv(rawCpv))
.where({ '@rid': existingRel['@rid'] })
.return('AFTER');
});
}
transaction.let(edgeName, (t) =>
t.create('edge', 'HasCPV')
.from(`$${tenderName}`)
.to(`$${cpvName}`)
.set(cpvExtractor.extractHasCpv(rawCpv)));
}
return cpvName;
}
Expand Down
Loading