From 53135acc05be5a8911a5eafbd85143b7b57c0c59 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 24 Jan 2021 14:01:36 +0100 Subject: [PATCH] Get/add/update/delete winners from lottery. --- api/controllers/lotteryWinnerController.js | 101 +++++++++ api/lottery.js | 236 +++++++++++++++++++-- 2 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 api/controllers/lotteryWinnerController.js diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js new file mode 100644 index 0000000..babfc9f --- /dev/null +++ b/api/controllers/lotteryWinnerController.js @@ -0,0 +1,101 @@ +const path = require("path"); +const lotteryRepository = require(path.join(__dirname, "../lottery")); + +const allWinners = (req, res) => { + const isAdmin = req.isAuthenticated() || true; + + return lotteryRepository + .allWinners(isAdmin) + .then(winners => + res.send({ + winners: winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch lottery winners." + }); + }); +}; + +const winnerById = (req, res) => { + const { id } = req.params; + + return lotteryRepository + .winnerById(id) + .then(winner => + res.send({ + winner, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured, unable to fetch winner by id.", + success: false + }); + }); +}; + +const deleteWinnerById = (req, res) => { + const isAdmin = req.isAuthenticated() || true; + const { id } = req.params; + + return lotteryRepository + .deleteWinnerById(id, isAdmin) + .then(removedWinner => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return removedWinner; + }) + .then(winner => + res.send({ + message: `Removed winner: ${winner.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing wine by id.", + success: false + }); + }); +}; + +const deleteWinners = (req, res) => { + return lotteryRepository + .deleteWinners() + .then(_ => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + }) + .then(_ => + res.send({ + message: "Removed all winners.", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleting wines", + success: false + }); + }); +}; + +module.exports = { + allWinners, + winnerById, + deleteWinnerById, + deleteWinners +}; diff --git a/api/lottery.js b/api/lottery.js index a748fc2..08e7f83 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -1,6 +1,10 @@ const path = require("path"); + const Attendee = require(path.join(__dirname, "/schemas/Attendee")); const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); +const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); + +const crypto = require("crypto"); class UserNotFound extends Error { constructor(message = "User not found.") { @@ -22,6 +26,33 @@ class WineNotFound extends Error { // TODO log missing user } +class WinnerNotFound extends Error { + constructor(message = "Winner not found.") { + super(message); + this.name = "WinnerNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + +class NoMoreAttendeesToWinError extends Error { + constructor(message = "No more attendees left to drawn from.") { + super(message); + this.name = "NoMoreAttendeesToWinError"; + this.statusCode = 404; + } +} + +class CouldNotFindNewWinnerAfterNTriesError extends Error { + constructor(tries) { + let message = `Could not a new winner after ${tries} tries.`; + super(message); + this.name = "CouldNotFindNewWinnerAfterNTriesError"; + this.statusCode = 404; + } +} + const redactAttendeeInfoMapper = attendee => { return { name: attendee.name, @@ -33,6 +64,13 @@ const redactAttendeeInfoMapper = attendee => { }; }; +const redactWinnerInfoMapper = winner => { + return { + name: winner.name, + color: winner.color + }; +}; + const allAttendees = isAdmin => { if (!isAdmin) { return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); @@ -63,15 +101,20 @@ const updateAttendeeById = (id, updateModel) => { throw new UserNotFound(); } + console.log(updateModel); + const updatedAttendee = { - name: updateModel.name || attendee.name, - green: updateModel.green || attendee.green, - red: updateModel.red || attendee.red, - blue: updateModel.blue || attendee.blue, - yellow: updateModel.yellow || attendee.yellow, - phoneNumber: updateModel.phoneNumber || attendee.phoneNumber + name: updateModel.name != null ? updateModel.name : attendee.name, + green: updateModel.green != null ? updateModel.green : attendee.green, + red: updateModel.red != null ? updateModel.red : attendee.red, + blue: updateModel.blue != null ? updateModel.blue : attendee.blue, + yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow, + phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber, + winner: updateModel.winner != null ? updateModel.winner : attendee.winner }; + console.log(updatedAttendee); + return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); }); }; @@ -119,13 +162,13 @@ const updateWineById = (id, updateModel) => { } const updatedWine = { - name: updateModel.name || wine.name, - vivinoLink: updateModel.vivinoLink || wine.vivinoLink, - rating: updateModel.rating || wine.rating, - image: updateModel.image || wine.image, - price: updateModel.price || wine.price, - country: updateModel.country || wine.country, - id: updateModel.id || wine.id + name: updateModel.name != null ? updateModel.name : wine.name, + vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, + rating: updateModel.rating != null ? updateModel.rating : wine.rating, + image: updateModel.image != null ? updateModel.image : wine.image, + price: updateModel.price != null ? updateModel.price : wine.price, + country: updateModel.country != null ? updateModel.country : wine.country, + id: updateModel.id != null ? updateModel.id : wine.id }; return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine); @@ -146,6 +189,166 @@ const deleteWines = () => { return PreLotteryWine.deleteMany(); }; +const allWinners = isAdmin => { + if (!isAdmin) { + return VirtualWinner.find().then(winners => winners.map(redactWinnerInfoMapper)); + } else { + return VirtualWinner.find(); + } +}; + +const winnerById = (id, isAdmin) => { + return VirtualWinner.findOne({ _id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + if (!isAdmin) { + return redactWinnerInfoMapper(winner); + } else { + return winner; + } + }); +}; + +const deleteWinnerById = id => { + return VirtualWinner.findOne({ _id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + return VirtualWinner.deleteOne({ _id: id }).then(_ => winner); + }); +}; + +const deleteWinners = () => { + return VirtualWinner.deleteMany(); +}; + +const drawWinner = async () => { + let allContestants = await Attendee.find({ winner: false }); + + if (allContestants.length == 0) { + throw new NoMoreAttendeesToWinError(); + } + + let raffleColors = []; + for (let i = 0; i < allContestants.length; i++) { + let currentContestant = allContestants[i]; + for (let blue = 0; blue < currentContestant.blue; blue++) { + raffleColors.push("blue"); + } + for (let red = 0; red < currentContestant.red; red++) { + raffleColors.push("red"); + } + for (let green = 0; green < currentContestant.green; green++) { + raffleColors.push("green"); + } + for (let yellow = 0; yellow < currentContestant.yellow; yellow++) { + raffleColors.push("yellow"); + } + } + + raffleColors = shuffle(raffleColors); + + let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)]; + let findObject = { winner: false }; + + findObject[colorToChooseFrom] = { $gt: 0 }; + + let tries = 0; + const maxTries = 3; + let contestantsToChooseFrom = undefined; + while (contestantsToChooseFrom == undefined && tries < maxTries) { + const hit = await Attendee.find(findObject); + if (hit && hit.length) { + contestantsToChooseFrom = hit; + break; + } + tries++; + } + if (contestantsToChooseFrom == undefined) { + throw new CouldNotFindNewWinnerAfterNTriesError(maxTries); + } + + let attendeeListDemocratic = []; + + let currentContestant; + for (let i = 0; i < contestantsToChooseFrom.length; i++) { + currentContestant = contestantsToChooseFrom[i]; + for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) { + attendeeListDemocratic.push({ + name: currentContestant.name, + phoneNumber: currentContestant.phoneNumber, + red: currentContestant.red, + blue: currentContestant.blue, + green: currentContestant.green, + yellow: currentContestant.yellow + }); + } + } + + attendeeListDemocratic = shuffle(attendeeListDemocratic); + + let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)]; + + let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ + timestamp_drawn: 1 + }); + + let newWinnerElement = new VirtualWinner({ + name: winner.name, + phoneNumber: winner.phoneNumber, + color: colorToChooseFrom, + red: winner.red, + blue: winner.blue, + green: winner.green, + yellow: winner.yellow, + id: sha512(winner.phoneNumber, genRandomString(10)), + timestamp_drawn: new Date().getTime() + }); + + await newWinnerElement.save(); + await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } }); + + return { winner, color: colorToChooseFrom, winners }; +}; + +/** - - UTILS - - **/ +const genRandomString = function(length) { + return crypto + .randomBytes(Math.ceil(length / 2)) + .toString("hex") /** convert to hexadecimal format */ + .slice(0, length); /** return required number of characters */ +}; + +const sha512 = function(password, salt) { + var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */ + hash.update(password); + var value = hash.digest("hex"); + return value; +}; + +function shuffle(array) { + let currentIndex = array.length, + temporaryValue, + randomIndex; + + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + module.exports = { allAttendees, addAttendee, @@ -156,5 +359,10 @@ module.exports = { addWines, updateWineById, deleteWineById, - deleteWines + deleteWines, + allWinners, + winnerById, + deleteWinnerById, + deleteWinners, + drawWinner };