diff --git a/api/attendee.js b/api/attendee.js new file mode 100644 index 0000000..e26cbf8 --- /dev/null +++ b/api/attendee.js @@ -0,0 +1,81 @@ +const path = require("path"); + +const Attendee = require(path.join(__dirname, "/schemas/Attendee")); +const { UserNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const redactAttendeeInfoMapper = attendee => { + return { + name: attendee.name, + raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, + red: attendee.red, + blue: attendee.blue, + green: attendee.green, + yellow: attendee.yellow + }; +}; + +const allAttendees = (isAdmin = false) => { + if (!isAdmin) { + return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); + } else { + return Attendee.find(); + } +}; + +const addAttendee = attendee => { + const { name, red, blue, green, yellow, phoneNumber } = attendee; + + let newAttendee = new Attendee({ + name, + red, + blue, + green, + yellow, + phoneNumber, + winner: false + }); + + return newAttendee.save().then(_ => newAttendee); +}; + +const updateAttendeeById = (id, updateModel) => { + return Attendee.findOne({ _id: id }).then(attendee => { + if (attendee == null) { + throw new UserNotFound(); + } + + const updatedAttendee = { + 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 + }; + + return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); + }); +}; + +const deleteAttendeeById = id => { + return Attendee.findOne({ _id: id }).then(attendee => { + if (attendee == null) { + throw new UserNotFound(); + } + + return Attendee.deleteOne({ _id: id }).then(_ => attendee); + }); +}; + +const deleteAttendees = () => { + return Attendee.deleteMany(); +}; + +module.exports = { + allAttendees, + addAttendee, + updateAttendeeById, + deleteAttendeeById, + deleteAttendees +}; diff --git a/api/chatHistory.js b/api/controllers/chatController.js similarity index 64% rename from api/chatHistory.js rename to api/controllers/chatController.js index 9578176..b680cb7 100644 --- a/api/chatHistory.js +++ b/api/controllers/chatController.js @@ -1,5 +1,6 @@ const path = require("path"); -const { history, clearHistory } = require(path.join(__dirname + "/../api/redis")); +const { history, clearHistory } = require(path.join(__dirname + "/../redis")); +console.log("loading chat"); const getAllHistory = (req, res) => { let { page, limit } = req.query; @@ -8,19 +9,23 @@ const getAllHistory = (req, res) => { return history(page, limit) .then(messages => res.json(messages)) - .catch(error => res.status(500).json({ - message: error.message, - success: false - })); + .catch(error => + res.status(500).json({ + message: error.message, + success: false + }) + ); }; const deleteHistory = (req, res) => { return clearHistory() .then(message => res.json(message)) - .catch(error => res.status(500).json({ - message: error.message, - success: false - })); + .catch(error => + res.status(500).json({ + message: error.message, + success: false + }) + ); }; module.exports = { diff --git a/api/controllers/historyController.js b/api/controllers/historyController.js new file mode 100644 index 0000000..777ed72 --- /dev/null +++ b/api/controllers/historyController.js @@ -0,0 +1,261 @@ +const path = require("path"); +const historyRepository = require(path.join(__dirname, "../history")); + +const sortOptions = ["desc", "asc"]; +const includeWinesOptions = ["true", "false"]; + +const all = (req, res) => { + const { sort, includeWines } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return historyRepository + .all(includeWines == "true") + .then(winners => + res.send({ + winners: sort !== "asc" ? winners : winners.reverse(), + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winners." + }); + }); +}; + +const byDate = (req, res) => { + let { date } = req.params; + + const regexDate = new RegExp("^\\d{4}-\\d{2}-\\d{2}$"); + if (!isNaN(date)) { + date = new Date(new Date(parseInt(date * 1000)).setHours(0, 0, 0, 0)); + } else if (regexDate.test(date)) { + date = new Date(date); + } else if (date !== undefined) { + return res.status(400).send({ + message: "Invalid date parameter, allowed epoch seconds or YYYY-MM-DD.", + success: false + }); + } + + return historyRepository + .byDate(date) + .then(winners => + res.send({ + date: date, + winners: winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by date." + }); + }); +}; + +const groupByDate = (req, res) => { + const { sort, includeWines } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return historyRepository + .groupByDate(includeWines == "true", sort) + .then(lotteries => + res.send({ + lotteries: lotteries, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by date." + }); + }); +}; + +const latest = (req, res) => { + return historyRepository + .latest() + .then(winners => + res.send({ + ...winners, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by date." + }); + }); +}; + +const byName = (req, res) => { + const { name } = req.params; + const { sort } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + return historyRepository + .byName(name, sort) + .then(winner => + res.send({ + winner: winner, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winner by name." + }); + }); +}; + +const search = (req, res) => { + const { name, sort } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } + + return historyRepository + .search(name, sort) + .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 winner by name." + }); + }); +}; + +const groupByColor = (req, res) => { + const { includeWines } = req.query; + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + return historyRepository + .groupByColor(includeWines == "true") + .then(colors => + res.send({ + colors: colors, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch winners by color." + }); + }); +}; + +const orderByWins = (req, res) => { + let { includeWines, limit } = req.query; + + if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { + return res.status(400).send({ + message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, + success: false + }); + } + + if (limit && isNaN(limit)) { + return res.status(400).send({ + message: "If limit query parameter is provided it must be a number", + success: false + }); + } else if (!!!isNaN(limit)) { + limit = Number(limit); + } + + return historyRepository + .orderByWins(includeWines == "true", limit) + .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 winners by color." + }); + }); +}; + +module.exports = { + all, + byDate, + groupByDate, + latest, + byName, + search, + groupByColor, + orderByWins +}; diff --git a/api/controllers/lotteryAttendeeController.js b/api/controllers/lotteryAttendeeController.js new file mode 100644 index 0000000..911aa16 --- /dev/null +++ b/api/controllers/lotteryAttendeeController.js @@ -0,0 +1,135 @@ +const path = require("path"); +const attendeeRepository = require(path.join(__dirname, "../attendee")); + +const allAttendees = (req, res) => { + const isAdmin = req.isAuthenticated(); + + return attendeeRepository + .allAttendees(isAdmin) + .then(attendees => + res.send({ + attendees: attendees, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch lottery attendees." + }); + }); +}; + +const addAttendee = (req, res) => { + const { attendee } = req.body; + + const requiredColors = [attendee["red"], attendee["blue"], attendee["green"], attendee["yellow"]]; + const correctColorsTypes = requiredColors.filter(color => typeof color === "number"); + if (requiredColors.length !== correctColorsTypes.length) { + return res.status(400).send({ + message: "Incorrect or missing color, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.", + success: false + }); + } + + if (typeof attendee["name"] !== "string" || typeof attendee["phoneNumber"] !== "number") { + return res.status(400).send({ + message: "Incorrect or missing attendee keys 'name' or 'phoneNumber'.", + success: false + }); + } + + return attendeeRepository + .addAttendee(attendee) + .then(savedAttendee => { + var io = req.app.get("socketio"); + io.emit("new_attendee", {}); + return savedAttendee; + }) + .then(savedAttendee => + res.send({ + attendee: savedAttendee, + message: `Successfully added attendee ${attendee.name} to lottery.`, + success: true + }) + ); +}; + +const updateAttendeeById = (req, res) => { + const { id } = req.params; + const { attendee } = req.body; + + return attendeeRepository + .updateAttendeeById(id, attendee) + .then(updatedAttendee => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return updatedAttendee; + }) + .then(attendee => + res.send({ + attendee, + message: `Updated attendee: ${attendee.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing attendee by id.", + success: false + }); + }); +}; + +const deleteAttendeeById = (req, res) => { + const { id } = req.params; + + return attendeeRepository + .deleteAttendeeById(id) + .then(removedAttendee => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return removedAttendee; + }) + .then(attendee => + res.send({ + message: `Removed attendee: ${attendee.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleteing attendee by id.", + success: false + }); + }); +}; + +const deleteAttendees = (req, res) => { + return attendeeRepository + .deleteAttendees() + .then(removedAttendee => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + }) + .then(_ => + res.send({ + message: "Removed all attendees", + success: true + }) + ); +}; + +module.exports = { + allAttendees, + addAttendee, + updateAttendeeById, + deleteAttendeeById, + deleteAttendees +}; diff --git a/api/controllers/lotteryController.js b/api/controllers/lotteryController.js new file mode 100644 index 0000000..ab446cc --- /dev/null +++ b/api/controllers/lotteryController.js @@ -0,0 +1,192 @@ +const path = require("path"); +const lotteryRepository = require(path.join(__dirname, "../lottery")); + +const drawWinner = (req, res) => { + return lotteryRepository + .drawWinner() + .then(({ winner, color, winners }) => { + var io = req.app.get("socketio"); + io.emit("winner", { + color: color, + name: winner.name, + winner_count: winners.length + 1 + }); + + return { winner, color, winners }; + }) + .then(({ winner, color, winners }) => + res.send({ + color: color, + winner: winner, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while drawing winner.", + success: false + }); + }); +}; + +const archiveLottery = (req, res) => { + const { lottery } = req.body; + if (lottery == undefined || !lottery instanceof Object) { + return res.status(400).send({ + message: "Missing lottery object.", + success: false + }); + } + + let { stolen, date, raffles, wines } = lottery; + stolen = stolen !== undefined ? stolen : 0; // default = 0 + + const validDateFormat = new RegExp("d{4}-d{2}-d{2}"); + if (date != undefined && (!validDateFormat.test(date) || isNaN(date))) { + return res.status(400).send({ + message: "Date must be defined as 'yyyy-mm-dd'.", + success: false + }); + } else if (date != undefined) { + date = Date.parse(date, "yyyy-MM-dd"); + } else { + date = new Date(); + } + + return verifyLotteryPayload(raffles, stolen, wines) + .then(_ => lotteryRepository.archive(date, raffles, stolen, wines)) + .then(_ => + res.send({ + message: "Successfully archive lottery", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while submitting lottery.", + success: false + }); + }); +}; + +const lotteryByDate = (req, res) => { + const { epoch } = req.params; + + if (!/^\d+$/.test(epoch)) { + return res.status(400).send({ + message: "Last parameter must be epoch (in seconds).", + success: false + }); + } + const date = new Date(Number(epoch) * 1000); + + return lotteryRepository + .lotteryByDate(date) + .then(lottery => + res.send({ + lottery, + message: `Lottery for date: ${dateToDateString(date)}/${epoch}.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching lottery by date.", + success: false + }); + }); +}; + +const sortOptions = ["desc", "asc"]; +const allLotteries = (req, res) => { + let { includeWinners, year, sort } = req.query; + + if (sort !== undefined && !sortOptions.includes(sort)) { + return res.status(400).send({ + message: `Sort option must be: '${sortOptions.join(", ")}'`, + success: false + }); + } else if (sort === undefined) { + sort = "asc"; + } + + let allLotteriesFunction = lotteryRepository.allLotteries; + if (includeWinners === "true") { + allLotteriesFunction = lotteryRepository.allLotteriesIncludingWinners; + } + + return allLotteriesFunction(sort, year) + .then(lotteries => + res.send({ + lotteries, + message: "All lotteries.", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching all lotteries.", + success: false + }); + }); +}; + +function verifyLotteryPayload(raffles, stolen, wines) { + return new Promise((resolve, reject) => { + if (raffles == undefined || !raffles instanceof Array) { + reject({ + message: "Raffles must be array.", + status: 400 + }); + } + + const requiredColors = [raffles["red"], raffles["blue"], raffles["green"], raffles["yellow"]]; + const correctColorsTypes = requiredColors.filter(color => typeof color === "number"); + if (requiredColors.length !== correctColorsTypes.length) { + reject({ + message: + "Incorrect or missing raffle colors, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.", + status: 400 + }); + } + + if (stolen == undefined || (isNaN(stolen) && stolen >= 0)) { + reject({ + message: "Number of stolen raffles must be positive integer or 0.", + status: 400 + }); + } + + if (wines == undefined || !wines instanceof Array) { + reject({ + message: "Wines must be array.", + status: 400 + }); + } + + resolve(); + }); +} + +function dateToDateString(date) { + const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); + const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); + const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); + + return `${ye}-${mo}-${da}`; +} + +module.exports = { + drawWinner, + archiveLottery, + lotteryByDate, + allLotteries +}; diff --git a/api/controllers/lotteryWineController.js b/api/controllers/lotteryWineController.js new file mode 100644 index 0000000..38685ed --- /dev/null +++ b/api/controllers/lotteryWineController.js @@ -0,0 +1,207 @@ +const path = require("path"); +const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine")); + +const allWines = (req, res) => { + return prelotteryWineRepository + .allWines() + .then(wines => + res.send({ + wines: wines, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch lottery wines." + }); + }); +}; + +const addWines = (req, res) => { + let { wines } = req.body; + + if (!(wines instanceof Array)) { + return res.status(400).send({ + message: "Wines must be array.", + success: false + }); + } + + const validateAllWines = wines => + wines.map(wine => { + const requiredAttributes = ["name", "vivinoLink", "image", "id", "price"]; + + return Promise.all( + requiredAttributes.map(attr => { + if (typeof wine[attr] === "undefined" || wine[attr] == "") { + return Promise.reject({ + message: `Incorrect or missing attribute: ${attr}.`, + statusCode: 400, + success: false + }); + } + return Promise.resolve(); + }) + ).then(_ => Promise.resolve(wine)); + }); + + return Promise.all(validateAllWines(wines)) + .then(wines => prelotteryWineRepository.addWines(wines)) + .then(savedWines => { + var io = req.app.get("socketio"); + io.emit("new_wine", {}); + return true; + }) + .then(success => + res.send({ + message: `Successfully added wines to lottery.`, + success: success + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured adding wines.", + success: false + }); + }); +}; + +const wineById = (req, res) => { + const { id } = req.params; + + return prelotteryWineRepository + .wineById(id) + .then(wine => + res.send({ + wine, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching wine by id.", + success: false + }); + }); +}; + +const updateWineById = (req, res) => { + const { id } = req.params; + const { wine } = req.body; + + if (id == null || id == "undefined") { + return res.status(400).send({ + message: "Unable to update without id.", + success: false + }); + } + + return prelotteryWineRepository + .updateWineById(id, wine) + .then(updatedWine => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return updatedWine; + }) + .then(wine => + res.send({ + wine, + message: `Updated wine: ${wine.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 deleteWineById = (req, res) => { + const { id } = req.params; + + return prelotteryWineRepository + .deleteWineById(id) + .then(removedWine => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + return removedWine; + }) + .then(wine => + res.send({ + message: `Removed wine: ${wine.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 deleteWines = (req, res) => { + return prelotteryWineRepository + .deleteWines() + .then(_ => { + var io = req.app.get("socketio"); + io.emit("refresh_data", {}); + }) + .then(_ => + res.send({ + message: "Removed all wines.", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while deleting wines", + success: false + }); + }); +}; + +const wineSchema = (req, res) => { + return prelotteryWineRepository + .wineSchema() + .then(schema => + res.send({ + schema: schema, + message: `Wine schema template.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch wine schema template." + }); + }); +}; + +module.exports = { + allWines, + addWines, + wineById, + updateWineById, + deleteWineById, + deleteWines, + wineSchema +}; diff --git a/api/controllers/lotteryWinnerController.js b/api/controllers/lotteryWinnerController.js new file mode 100644 index 0000000..1923d2e --- /dev/null +++ b/api/controllers/lotteryWinnerController.js @@ -0,0 +1,195 @@ +const path = require("path"); +const winnerRepository = require(path.join(__dirname, "../winner")); +const { WinnerNotFound } = require(path.join(__dirname, "../vinlottisErrors")); +const prizeDistributionRepository = require(path.join(__dirname, "../prizeDistribution")); + +// should not be used, is done through POST /lottery/prize-distribution/prize/:id - claimPrize. +const addWinners = (req, res) => { + const { winners } = req.body; + + if (!(winners instanceof Array)) { + return res.status(400).send({ + message: "Winners must be array.", + success: false + }); + } + + const requiredAttributes = ["name", "color", "wine"]; + const validColors = ["red", "blue", "green", "yellow"]; + const validateAllWinners = winners => + winners.map(winner => { + return Promise.all( + requiredAttributes.map(attr => { + if (typeof winner[attr] === "undefined") { + return Promise.reject({ + message: `Incorrect or missing attribute: ${attr}.`, + statusCode: 400 + }); + } + + if (!validColors.includes(winner.color)) { + return Promise.reject({ + message: `Missing or incorrect color value, must have one of values: ${validColors.join(", ")}.`, + statusCode: 400 + }); + } + + return Promise.resolve(); + }) + ).then(_ => Promise.resolve(winner)); + }); + + return Promise.all(validateAllWinners(winners)) + .then(winners => + winners.map(winner => { + return prizeDistributionRepository.claimPrize(winner, winner.wine); + }) + ) + .then(winners => + res.send({ + winners: winners, + message: `Successfully added winners to lottery.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured adding winners.", + success: false + }); + }); +}; + +const allWinners = (req, res) => { + const isAdmin = req.isAuthenticated(); + + return winnerRepository + .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; + const isAdmin = req.isAuthenticated(); + + return winnerRepository + .winnerById(id, isAdmin) + .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 updateWinnerById = (req, res) => { + const { id } = req.params; + const { winner } = req.body; + + if (id == null || id == "undefined") { + return res.status(400).send({ + message: "Unable to update without id.", + success: false + }); + } + + return winnerRepository + .updateWinnerById(id, winner) + .then(winner => + res.send({ + winner, + message: `Updated winner: ${winner.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while updating winner by id.", + success: false + }); + }); +}; + +const deleteWinnerById = (req, res) => { + const isAdmin = req.isAuthenticated(); + const { id } = req.params; + + return winnerRepository + .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 winnerRepository + .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 = { + addWinners, + allWinners, + winnerById, + updateWinnerById, + deleteWinnerById, + deleteWinners +}; diff --git a/api/controllers/messageController.js b/api/controllers/messageController.js new file mode 100644 index 0000000..94dffe9 --- /dev/null +++ b/api/controllers/messageController.js @@ -0,0 +1,30 @@ +const path = require("path"); +const messageRepository = require(path.join(__dirname, "../message")); +const winnerRepository = require(path.join(__dirname, "../winner")); + +const notifyWinnerById = (req, res) => { + const { id } = req.params; + const isAdmin = req.isAuthenticated(); + + return winnerRepository + .winnerById(id, isAdmin) + .then(winner => messageRepository.sendPrizeSelectionLink(winner)) + .then(messageResponse => + res.send({ + messageResponse, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while sending message to winner by id.", + success: false + }); + }); +}; + +module.exports = { + notifyWinnerById +}; diff --git a/api/controllers/prizeDistributionController.js b/api/controllers/prizeDistributionController.js new file mode 100644 index 0000000..7e9b840 --- /dev/null +++ b/api/controllers/prizeDistributionController.js @@ -0,0 +1,104 @@ +const path = require("path"); + +const prizeDistribution = require(path.join(__dirname, "../prizeDistribution")); +const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine")); +const winnerRepository = require(path.join(__dirname, "../winner")); +const message = require(path.join(__dirname, "../message")); + +const start = async (req, res) => { + const allWinners = await winnerRepository.allWinners(true); + if (allWinners.length === 0) { + return res.status(503).send({ + message: "No winners found to distribute prizes to.", + success: false + }); + } + + const laterWinners = allWinners.slice(1); + + return prizeDistribution + .notifyNextWinner() + .then(_ => message.sendInitialMessageToWinners(laterWinners)) + .then(_ => + res.send({ + message: `Send link to first winner and notified everyone else.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while starting prize distribution.", + success: false + }); + }); +}; + +const getPrizesForWinnerById = (req, res) => { + const { id } = req.params; + + return prizeDistribution + .verifyWinnerNextInLine(id) + .then(winner => { + return prelotteryWineRepository.allWinesWithoutWinner().then(wines => [wines, winner]); + }) + .then(([wines, winner]) => + res.send({ + wines: wines, + winner: winner, + message: "Wines to select from", + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching prizes.", + success: false + }); + }); +}; + +const submitPrizeForWinnerById = async (req, res) => { + const { id } = req.params; + const { wine } = req.body; + + let prelotteryWine, winner; + try { + prelotteryWine = await prelotteryWineRepository.wineById(wine._id); + winner = await winnerRepository.winnerById(id, true); + } catch (error) { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while claiming prize.", + success: false + }); + } + + return prizeDistribution + .claimPrize(prelotteryWine, winner) + .then(_ => prizeDistribution.notifyNextWinner()) + .then(_ => + res.send({ + message: `${winner.name} successfully claimed prize: ${prelotteryWine.name}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while claiming prize.", + success: false + }); + }); +}; + +module.exports = { + start, + getPrizesForWinnerById, + submitPrizeForWinnerById +}; diff --git a/api/controllers/requestController.js b/api/controllers/requestController.js new file mode 100644 index 0000000..7144674 --- /dev/null +++ b/api/controllers/requestController.js @@ -0,0 +1,104 @@ +const path = require("path"); +const requestRepository = require(path.join(__dirname, "../request")); + +function addRequest(req, res) { + const { wine } = req.body; + + return verifyWineValues(wine) + .then(_ => requestRepository.addNew(wine)) + .then(wine => + res.json({ + message: "Successfully added new request", + wine: wine, + success: true + }) + ) + .catch(error => { + const { message, statusCode } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to add requested wine." + }); + }); +} + +function allRequests(req, res) { + return requestRepository + .getAll() + .then(wines => + res.json({ + wines: wines, + success: true + }) + ) + .catch(error => { + const { message, statusCode } = error; + return res.status(statusCode || 500).json({ + success: false, + message: message || "Unable to fetch all requested wines." + }); + }); +} + +function deleteRequest(req, res) { + const { id } = req.params; + + return requestRepository + .deleteById(id) + .then(_ => + res.json({ + message: `Slettet vin med id: ${id}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to delete requested wine." + }); + }); +} + +function verifyWineValues(wine) { + return new Promise((resolve, reject) => { + if (wine == undefined) { + reject({ + message: "No wine object found in request body.", + status: 400 + }); + } + + if (wine.id == null) { + reject({ + message: "Wine object missing value id.", + status: 400 + }); + } else if (wine.name == null) { + reject({ + message: "Wine object missing value name.", + status: 400 + }); + } else if (wine.vivinoLink == null) { + reject({ + message: "Wine object missing value vivinoLink.", + status: 400 + }); + } else if (wine.image == null) { + reject({ + message: "Wine object missing value image.", + status: 400 + }); + } + + resolve(); + }); +} + +module.exports = { + addRequest, + allRequests, + deleteRequest +}; diff --git a/api/controllers/userController.js b/api/controllers/userController.js new file mode 100644 index 0000000..b12b2f5 --- /dev/null +++ b/api/controllers/userController.js @@ -0,0 +1,55 @@ +const path = require("path"); +const userRepository = require(path.join(__dirname, "../user")); + +function register(req, res, next) { + const { username, password } = req.body; + + return userRepository + .register(username, password) + .then(user => userRepository.login(req, user)) + .then(_ => + res.send({ + messsage: `Bruker registrert. Velkommen ${username}`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unable to sign in with given username and passowrd", + success: false + }); + }); +} + +const login = (req, res, next) => { + return userRepository + .authenticate(req) + .then(user => userRepository.login(req, user)) + .then(user => { + res.send({ + message: `Velkommen ${user.username}`, + success: true + }); + }) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unable to sign in with given username and passowrd", + success: false + }); + }); +}; + +const logout = (req, res) => { + req.logout(); + res.redirect("/"); +}; + +module.exports = { + register, + login, + logout +}; diff --git a/api/controllers/vinmonopoletController.js b/api/controllers/vinmonopoletController.js new file mode 100644 index 0000000..b68ec51 --- /dev/null +++ b/api/controllers/vinmonopoletController.js @@ -0,0 +1,85 @@ +const path = require("path"); +const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet")); + +function searchWines(req, res) { + const { name, page } = req.query; + + return vinmonopoletRepository.searchWinesByName(name, page).then(wines => + res.json({ + wines: wines, + count: wines.length, + page: page, + success: true + }) + ); +} + +function wineByEAN(req, res) { + const { ean } = req.params; + + return vinmonopoletRepository.searchByEAN(ean).then(wines => + res.json({ + wines: wines, + success: true + }) + ); +} + +function wineById(req, res) { + const { id } = req.params; + + return vinmonopoletRepository.wineById(id).then(wines => + res.json({ + wine: wines[0], + success: true + }) + ); +} + +function allStores(req, res) { + return vinmonopoletRepository + .allStores() + .then(stores => + res.send({ + stores, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetch all vinmonopolet stores.", + success: false + }); + }); +} + +function searchStores(req, res) { + const { name } = req.query; + + return vinmonopoletRepository + .searchStoresByName(name) + .then(stores => + res.send({ + stores, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetch all vinmonopolet stores.", + success: false + }); + }); +} + +module.exports = { + searchWines, + wineByEAN, + wineById, + allStores, + searchStores +}; diff --git a/api/controllers/wineController.js b/api/controllers/wineController.js new file mode 100644 index 0000000..677bee6 --- /dev/null +++ b/api/controllers/wineController.js @@ -0,0 +1,60 @@ +const path = require("path"); +const wineRepository = require(path.join(__dirname, "../wine")); + +const allWines = (req, res) => { + // TODO add "includeWinners" + let { limit } = req.query; + + if (limit && isNaN(limit)) { + return res.status(400).send({ + message: "If limit query parameter is provided it must be a number", + success: false + }); + } else if (!!!isNaN(limit)) { + limit = Number(limit); + } + + return wineRepository + .allWines(limit) + .then(wines => + res.send({ + wines: wines, + message: `All wines.`, + success: true + }) + ) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + success: false, + message: message || "Unable to fetch all wines." + }); + }); +}; + +const wineById = (req, res) => { + const { id } = req.params; + + return wineRepository + .wineById(id) + .then(wine => { + res.send({ + wine, + success: true + }); + }) + .catch(error => { + const { statusCode, message } = error; + + return res.status(statusCode || 500).send({ + message: message || "Unexpected error occured while fetching wine by id.", + success: false + }); + }); +}; + +module.exports = { + allWines, + wineById +}; diff --git a/api/history.js b/api/history.js new file mode 100644 index 0000000..46195ce --- /dev/null +++ b/api/history.js @@ -0,0 +1,349 @@ +const path = require("path"); + +const Winner = require(path.join(__dirname, "/schemas/Highscore")); +const wineRepository = require(path.join(__dirname, "/wine")); + +class HistoryByDateNotFound extends Error { + constructor(message = "History for given date not found.") { + super(message); + this.name = "HistoryByDateNotFound"; + this.statusCode = 404; + } +} + +class HistoryForUserNotFound extends Error { + constructor(message = "History for given user not found.") { + super(message); + this.name = "HistoryForUserNotFound"; + this.statusCode = 404; + } +} + +// highscore +const addWinnerWithWine = async (winner, wine) => { + const exisitingWinner = await Winner.findOne({ + name: winner.name + }); + const savedWine = await wineRepository.addWine(wine); + + const date = new Date(); + date.setHours(5, 0, 0, 0); + const winObject = { + date: date, + wine: savedWine, + color: winner.color + }; + + if (exisitingWinner == undefined) { + const newWinner = new Winner({ + name: winner.name, + wins: [winObject] + }); + + await newWinner.save(); + } else { + exisitingWinner.wins.push(winObject); + exisitingWinner.markModified("wins"); + await exisitingWinner.save(); + } + + return exisitingWinner; +}; + +// lottery +const all = (includeWines = false) => { + if (includeWines === false) { + return Winner.find().sort("-wins.date"); + } else { + return Winner.find() + .sort("-wins.date") + .populate("wins.wine"); + } +}; + +// lottery +const byDate = date => { + const startQueryDate = new Date(date.setHours(0, 0, 0, 0)); + const endQueryDate = new Date(date.setHours(24, 59, 59, 99)); + const query = [ + { + $match: { + "wins.date": { + $gte: startQueryDate, + $lte: endQueryDate + } + } + }, + { $unwind: "$wins" }, + { + $match: { + "wins.date": { + $gte: startQueryDate, + $lte: endQueryDate + } + } + }, + { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }, + { $unwind: "$wins.wine" }, + { + $project: { + name: "$name", + date: "$wins.date", + color: "$wins.color", + wine: "$wins.wine" + } + } + ]; + + return Winner.aggregate(query).then(winners => { + if (winners.length == 0) { + throw new HistoryByDateNotFound(); + } + return winners; + }); +}; + +// highscore +const byName = (name, sort = "desc") => { + return Winner.findOne({ name }, ["name", "wins"]) + .sort("-wins.date") + .populate("wins.wine") + .then(winner => { + if (winner) { + winner.wins = sort !== "asc" ? winner.wins.reverse() : winner.wins; + return winner; + } else { + throw new HistoryForUserNotFound(); + } + }); +}; + +// highscore +const search = (query, sort = "desc") => { + return Winner.find({ name: { $regex: query, $options: "i" } }, ["name"]).then(winners => { + if (winners) { + winners = sort === "desc" ? winners.reverse() : winners; + return winners; + } else { + throw new HistoryForUserNotFound(); + } + }); +}; + +// lottery +const latest = () => { + const query = [ + { + $unwind: "$wins" + }, + { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }, + { + $group: { + _id: "$wins.date", + winners: { + $push: { + _id: "$_id", + name: "$name", + color: "$wins.color", + wine: "$wins.wine" + } + } + } + }, + { + $project: { + date: "$_id", + winners: "$winners" + } + }, + { + $sort: { + _id: -1 + } + }, + { + $limit: 1 + } + ]; + + return Winner.aggregate(query).then(winners => winners[0]); +}; + +// lottery - byDate +const groupByDate = (includeWines = false, sort = "asc") => { + const sortDirection = sort == "asc" ? -1 : 1; + const query = [ + { + $unwind: "$wins" + }, + { + $group: { + _id: "$wins.date", + winners: { + $push: { + _id: "$_id", + name: "$name", + color: "$wins.color", + wine: "$wins.wine" + } + } + } + }, + { + $project: { + date: "$_id", + winners: "$winners" + } + }, + { + $sort: { + date: sortDirection + } + } + ]; + + if (includeWines) { + query.splice(1, 0, { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }); + } + + return Winner.aggregate(query); +}; + +// highscore - byColor +const groupByColor = (includeWines = false) => { + const query = [ + { + $unwind: "$wins" + }, + { + $group: { + _id: "$wins.color", + winners: { + $push: { + _id: "$_id", + name: "$name", + date: "$wins.date", + wine: "$wins.wine" + } + }, + count: { $sum: 1 } + } + }, + { + $project: { + color: "$_id", + count: "$count", + winners: "$winners" + } + }, + { + $sort: { + _id: -1 + } + } + ]; + + if (includeWines) { + query.splice(1, 0, { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }); + } + + return Winner.aggregate(query); +}; + +// highscore - byWineOccurences + +// highscore - byWinCount +const orderByWins = (includeWines = false, limit = undefined) => { + let query = [ + { + $project: { + name: "$name", + wins: "$wins", + totalWins: { $size: "$wins" } + } + }, + { + $sort: { + totalWins: -1, + "wins.date": -1 + } + } + ]; + + if (includeWines) { + const includeWinesSubQuery = [ + { + $unwind: "$wins" + }, + { + $lookup: { + from: "wines", + localField: "wins.wine", + foreignField: "_id", + as: "wins.wine" + } + }, + { + $unwind: "$wins._id" + }, + { + $group: { + _id: "$_id", + name: { $first: "$name" }, + totalWins: { $first: "$totalWins" }, + wins: { $push: "$wins" } + } + } + ]; + + query = includeWinesSubQuery.concat(query); + } + + return Winner.aggregate(query).then(winners => { + if (limit == null) { + return winners; + } + + return winners.slice(0, limit); + }); +}; + +module.exports = { + addWinnerWithWine, + all, + byDate, + byName, + search, + latest, + groupByDate, + groupByColor, + orderByWins +}; diff --git a/api/lottery.js b/api/lottery.js index 6783a8c..6b1bf90 100644 --- a/api/lottery.js +++ b/api/lottery.js @@ -1,132 +1,263 @@ -const path = require('path'); +const path = require("path"); +const crypto = require("crypto"); -const Highscore = require(path.join(__dirname, '/schemas/Highscore')); -const Wine = require(path.join(__dirname, '/schemas/Wine')); +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 Lottery = require(path.join(__dirname, "/schemas/Purchase")); -// Utils -const epochToDateString = date => new Date(parseInt(date)).toDateString(); +const Message = require(path.join(__dirname, "/message")); +const historyRepository = require(path.join(__dirname, "/history")); +const wineRepository = require(path.join(__dirname, "/wine")); -const sortNewestFirst = (lotteries) => { - return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1) -} +const { + WinnerNotFound, + NoMoreAttendeesToWin, + CouldNotFindNewWinnerAfterNTries, + LotteryByDateNotFound +} = require(path.join(__dirname, "/vinlottisErrors")); -const groupHighscoreByDate = async (highscore=undefined) => { - if (highscore == undefined) - highscore = await Highscore.find(); +const archive = (date, raffles, stolen, wines) => { + const { blue, red, yellow, green } = raffles; + const bought = blue + red + yellow + green; - const highscoreByDate = []; - - highscore.forEach(person => { - person.wins.map(win => { - const epochDate = new Date(win.date).setHours(0,0,0,0); - const winnerObject = { - name: person.name, - color: win.color, - wine: win.wine, - date: epochDate - } - - const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate) - if (existingDateIndex > -1) - highscoreByDate[existingDateIndex].winners.push(winnerObject); - else - highscoreByDate.push({ - date: epochDate, - winners: [winnerObject] - }) - }) - }) - - return sortNewestFirst(highscoreByDate); -} - -const resolveWineReferences = (highscoreObject, key) => { - const listWithWines = highscoreObject[key] - - return Promise.all(listWithWines.map(element => - Wine.findById(element.wine) - .then(wine => { - element.wine = wine - return element - })) - ) - .then(resolvedListWithWines => { - highscoreObject[key] = resolvedListWithWines; - return highscoreObject - }) -} -// end utils - -// Routes -const all = (req, res) => { - return Highscore.find() - .then(highscore => groupHighscoreByDate(highscore)) - .then(lotteries => res.send({ - message: "Lotteries by date!", - lotteries - })) -} - -const latest = (req, res) => { - return groupHighscoreByDate() - .then(lotteries => lotteries.shift()) // first element in list - .then(latestLottery => resolveWineReferences(latestLottery, "winners")) - .then(lottery => res.send({ - message: "Latest lottery!", - winners: lottery.winners - }) - ) -} - -const byEpochDate = (req, res) => { - let { date } = req.params; - date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime() - const dateString = epochToDateString(date); - - return groupHighscoreByDate() - .then(lotteries => { - const lottery = lotteries.filter(lottery => lottery.date == date) - if (lottery.length > 0) { - return lottery[0] - } else { - return res.status(404).send({ - message: `No lottery found for date: ${ dateString }` - }) - } - }) - .then(lottery => resolveWineReferences(lottery, "winners")) - .then(lottery => res.send({ - message: `Lottery for date: ${ dateString}`, + return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => { + const lottery = new Lottery({ date, - winners: lottery.winners - })) -} + blue, + red, + yellow, + green, + bought, + stolen, + wines: resolvedWines + }); -const byName = (req, res) => { - const { name } = req.params; - const regexName = new RegExp(name, "i"); // lowercase regex of the name + return lottery.save(); + }); +}; - return Highscore.find({ name }) - .then(highscore => { - if (highscore.length > 0) { - return highscore[0] - } else { - return res.status(404).send({ - message: `Name: ${ name } not found in leaderboards.` - }) +const lotteryByDate = date => { + const startOfDay = new Date(date.setHours(0, 0, 0, 0)); + const endOfDay = new Date(date.setHours(24, 59, 59, 99)); + + const query = [ + { + $match: { + date: { + $gte: startOfDay, + $lte: endOfDay + } } - }) - .then(highscore => resolveWineReferences(highscore, "wins")) - .then(highscore => res.send({ - message: `Lottery winnings for name: ${ name }.`, - name: highscore.name, - highscore: sortNewestFirst(highscore.wins) - })) + }, + { + $lookup: { + from: "wines", + localField: "wines", + foreignField: "_id", + as: "wines" + } + } + ]; + + const aggregateLottery = Lottery.aggregate(query); + return aggregateLottery.project("-_id -__v").then(lotteries => { + if (lotteries.length == 0) { + throw new LotteryByDateNotFound(date); + } + return lotteries[0]; + }); +}; + +const allLotteries = (sort = "asc", yearFilter = undefined) => { + const sortDirection = sort == "asc" ? 1 : -1; + + let startQueryDate = new Date("1970-01-01"); + let endQueryDate = new Date("2999-01-01"); + if (yearFilter) { + startQueryDate = new Date(`${yearFilter}-01-01`); + endQueryDate = new Date(`${Number(yearFilter) + 1}-01-01`); + } + + const query = [ + { + $match: { + date: { + $gte: startQueryDate, + $lte: endQueryDate + } + } + }, + { + $sort: { + date: sortDirection + } + }, + { + $unset: ["_id", "__v"] + }, + { + $lookup: { + from: "wines", + localField: "wines", + foreignField: "_id", + as: "wines" + } + } + ]; + + return Lottery.aggregate(query); +}; + +const allLotteriesIncludingWinners = async (sort = "asc", yearFilter = undefined) => { + const lotteries = await allLotteries(sort, yearFilter); + const allWinners = await historyRepository.groupByDate(false, sort); + + return lotteries.map(lottery => { + const { winners } = allWinners.pop(); + + return { + wines: lottery.wines, + date: lottery.date, + blue: lottery.blue, + green: lottery.green, + yellow: lottery.yellow, + red: lottery.red, + bought: lottery.bought, + stolen: lottery.stolen, + winners: winners + }; + }); +}; + +const drawWinner = async () => { + let allContestants = await Attendee.find({ winner: false }); + + if (allContestants.length == 0) { + throw new NoMoreAttendeesToWin(); + } + + 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 CouldNotFindNewWinnerAfterNTries(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 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 } }); + + let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ + timestamp_drawn: 1 + }); + + 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 = { - all, - latest, - byEpochDate, - byName + drawWinner, + archive, + lotteryByDate, + allLotteries, + allLotteriesIncludingWinners }; diff --git a/api/message.js b/api/message.js index e4faf26..15d42da 100644 --- a/api/message.js +++ b/api/message.js @@ -2,34 +2,50 @@ const https = require("https"); const path = require("path"); const config = require(path.join(__dirname + "/../config/defaults/lottery")); -const dateString = (date) => { - if (typeof(date) == "string") { +const dateString = date => { + if (typeof date == "string") { date = new Date(date); } - const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date) - const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date) - const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date) + const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); + const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); + const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); - return `${da}-${mo}-${ye}` + return `${da}-${mo}-${ye}`; +}; + +async function sendInitialMessageToWinners(winners) { + const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` })); + + const body = { + sender: "Vinlottis", + message: "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!", + recipients: numbers + }; + + return gatewayRequest(body); } -async function sendWineSelectMessage(winnerObject) { - winnerObject.timestamp_sent = new Date().getTime(); - winnerObject.timestamp_limit = new Date().getTime() * 600000; - await winnerObject.save(); +async function sendPrizeSelectionLink(winner) { + winner.timestamp_sent = new Date().getTime(); + winner.timestamp_limit = new Date().getTime() + 1000 * 600; + await winner.save(); - let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin"); + const { id, name, phoneNumber } = winner; + const url = new URL(`/#/winner/${id}`, "https://lottis.vin"); + const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \ +å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \ +i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`; - return sendMessageToUser( - winnerObject.phoneNumber, - `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)` - ) + return sendMessageToNumber(phoneNumber, message); } async function sendWineConfirmation(winnerObject, wineObject, date) { date = dateString(date); - return sendMessageToUser(winnerObject.phoneNumber, - `Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`) + return sendMessageToNumber( + winnerObject.phoneNumber, + `Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\ +\nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!` + ); } async function sendLastWinnerMessage(winnerObject, wineObject) { @@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) { winnerObject.timestamp_limit = new Date().getTime(); await winnerObject.save(); - return sendMessageToUser( + return sendMessageToNumber( winnerObject.phoneNumber, - `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!` + `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \ +du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!` ); } async function sendWineSelectMessageTooLate(winnerObject) { - return sendMessageToUser( + return sendMessageToNumber( winnerObject.phoneNumber, - `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` + `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \ +puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` ); } -async function sendMessageToUser(phoneNumber, message) { - console.log(`Attempting to send message to ${ phoneNumber }.`) +async function sendMessageToNumber(phoneNumber, message) { + console.log(`Attempting to send message to ${phoneNumber}.`); const body = { sender: "Vinlottis", message: message, - recipients: [{ msisdn: `47${ phoneNumber }`}] + recipients: [{ msisdn: `47${phoneNumber}` }] }; return gatewayRequest(body); } - -async function sendInitialMessageToWinners(winners) { - let numbers = []; - for (let i = 0; i < winners.length; i++) { - numbers.push({ msisdn: `47${winners[i].phoneNumber}` }); - } - - const body = { - sender: "Vinlottis", - message: - "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!", - recipients: numbers - } - - return gatewayRequest(body); -} - async function gatewayRequest(body) { return new Promise((resolve, reject) => { const options = { hostname: "gatewayapi.com", post: 443, - path: `/rest/mtsms?token=${ config.gatewayToken }`, + path: `/rest/mtsms?token=${config.gatewayToken}`, method: "POST", headers: { "Content-Type": "application/json" } - } + }; - const req = https.request(options, (res) => { - console.log(`statusCode: ${ res.statusCode }`); - console.log(`statusMessage: ${ res.statusMessage }`); + const req = https.request(options, res => { + console.log(`statusCode: ${res.statusCode}`); + console.log(`statusMessage: ${res.statusMessage}`); - res.setEncoding('utf8'); + res.setEncoding("utf8"); if (res.statusCode == 200) { - res.on("data", (data) => { - console.log("Response from message gateway:", data) + res.on("data", data => { + console.log("Response from message gateway:", data); - resolve(JSON.parse(data)) + resolve(JSON.parse(data)); }); } else { - res.on("data", (data) => { + res.on("data", data => { data = JSON.parse(data); - return reject('Gateway error: ' + data['message'] || data) + return reject("Gateway error: " + data["message"] || data); }); } - }) + }); - req.on("error", (error) => { - console.error(`Error from sms service: ${ error }`); - reject(`Error from sms service: ${ error }`); - }) + req.on("error", error => { + console.error(`Error from sms service: ${error}`); + reject(`Error from sms service: ${error}`); + }); req.write(JSON.stringify(body)); req.end(); @@ -123,9 +124,9 @@ async function gatewayRequest(body) { } module.exports = { - sendWineSelectMessage, + sendInitialMessageToWinners, + sendPrizeSelectionLink, sendWineConfirmation, sendLastWinnerMessage, - sendWineSelectMessageTooLate, - sendInitialMessageToWinners -} + sendWineSelectMessageTooLate +}; diff --git a/api/middleware/alwaysAuthenticatedWhenLocalhost.js b/api/middleware/alwaysAuthenticatedWhenLocalhost.js new file mode 100644 index 0000000..c799ab3 --- /dev/null +++ b/api/middleware/alwaysAuthenticatedWhenLocalhost.js @@ -0,0 +1,6 @@ +const alwaysAuthenticatedWhenLocalhost = (req, res, next) => { + req.isAuthenticated = () => true; + return next(); +}; + +module.exports = alwaysAuthenticatedWhenLocalhost; diff --git a/api/middleware/mustBeAuthenticated.js b/api/middleware/mustBeAuthenticated.js index 77dfe87..2c44d0e 100644 --- a/api/middleware/mustBeAuthenticated.js +++ b/api/middleware/mustBeAuthenticated.js @@ -1,10 +1,4 @@ const mustBeAuthenticated = (req, res, next) => { - if (process.env.NODE_ENV == "development") { - console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`) - req.isAuthenticated = () => true; - return next(); - } - if (!req.isAuthenticated()) { return res.status(401).send({ success: false, diff --git a/api/prelotteryWine.js b/api/prelotteryWine.js new file mode 100644 index 0000000..cfbcfb5 --- /dev/null +++ b/api/prelotteryWine.js @@ -0,0 +1,103 @@ +const path = require("path"); + +const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); +const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const allWines = () => { + return PreLotteryWine.find().populate("winner"); +}; + +const allWinesWithoutWinner = () => { + return PreLotteryWine.find({ winner: { $exists: false } }); +}; + +const addWines = wines => { + const prelotteryWines = wines.map(wine => { + let newPrelotteryWine = new PreLotteryWine({ + name: wine.name, + vivinoLink: wine.vivinoLink, + rating: wine.rating, + year: wine.year, + image: wine.image, + price: wine.price, + country: wine.country, + id: wine.id + }); + + return newPrelotteryWine.save(); + }); + + return Promise.all(prelotteryWines); +}; + +const wineById = id => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + return wine; + }); +}; + +const updateWineById = (id, updateModel) => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + const updatedWine = { + name: updateModel.name != null ? updateModel.name : wine.name, + vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, + rating: updateModel.rating != null ? updateModel.rating : wine.rating, + year: updateModel.year != null ? updateModel.year : wine.year, + 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); + }); +}; + +const addWinnerToWine = (wine, winner) => { + wine.winner = winner; + winner.prize_selected = true; + return Promise.all([wine.save(), winner.save()]); +}; + +const deleteWineById = id => { + return PreLotteryWine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine); + }); +}; + +const deleteWines = () => { + return PreLotteryWine.deleteMany(); +}; + +const wineSchema = () => { + let schema = { ...PreLotteryWine.schema.obj }; + let nulledSchema = Object.keys(schema).reduce((accumulator, current) => { + accumulator[current] = ""; + return accumulator; + }, {}); + + return Promise.resolve(nulledSchema); +}; + +module.exports = { + allWines, + allWinesWithoutWinner, + addWines, + wineById, + addWinnerToWine, + updateWineById, + deleteWineById, + deleteWines, + wineSchema +}; diff --git a/api/prizeDistribution.js b/api/prizeDistribution.js new file mode 100644 index 0000000..e04eb89 --- /dev/null +++ b/api/prizeDistribution.js @@ -0,0 +1,110 @@ +const path = require("path"); + +const Wine = require(path.join(__dirname, "/schemas/Wine")); +const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); +const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); + +const message = require(path.join(__dirname, "/message")); +const historyRepository = require(path.join(__dirname, "/history")); +const winnerRepository = require(path.join(__dirname, "/winner")); +const wineRepository = require(path.join(__dirname, "/wine")); +const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine")); + +const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join( + __dirname, + "/vinlottisErrors" +)); + +const verifyWinnerNextInLine = async id => { + let foundWinner = await VirtualWinner.findOne({ id: id }); + + if (!foundWinner) { + throw new WinnerNotFound(); + } else if (foundWinner.timestamp_limit < new Date().getTime()) { + throw new WinnersTimelimitExpired(); + } + + let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); + + if ( + foundWinner.timestamp_limit == undefined || + foundWinner.timestamp_sent == undefined || + foundWinner.prize_selected == true + ) { + throw new WineSelectionWinnerNotNextInLine(); + } + + return Promise.resolve(foundWinner); +}; + +const claimPrize = (wine, winner) => { + return wineRepository + .addWine(wine) + .then(_ => prelotteryWineRepository.addWinnerToWine(wine, winner)) // prelotteryWine.deleteById + .then(_ => historyRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine + .then(_ => message.sendWineConfirmation(winner, wine)); +}; + +const notifyNextWinner = async () => { + let nextWinner = undefined; + + const winnersLeft = await VirtualWinner.find({ prize_selected: false }).sort({ timestamp_drawn: 1 }); + const winesLeft = await PreLotteryWine.find({ winner: { $exists: false } }); + + if (winnersLeft.length > 1) { + console.log("multiple winners left, choose next in line"); + nextWinner = winnersLeft[0]; // multiple winners left, choose next in line + } else if (winnersLeft.length == 1 && winesLeft.length > 1) { + console.log("one winner left, but multiple wines"); + nextWinner = winnersLeft[0]; // one winner left, but multiple wines + } else if (winnersLeft.length == 1 && winesLeft.length == 1) { + console.log("one winner and one wine left, choose for user"); + nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user + wine = winesLeft[0]; + return claimPrize(wine, nextWinner); + } + + if (nextWinner) { + return message.sendPrizeSelectionLink(nextWinner).then(_ => startTimeout(nextWinner.id)); + } else { + console.info("All winners notified. Could start cleanup here."); + return Promise.resolve({ + message: "All winners notified." + }); + } +}; + +// these need to be register somewhere to cancel if something +// goes wrong and we want to start prize distribution again +function startTimeout(id) { + const minute = 60000; + const minutesForTimeout = 10; + + console.log(`Starting timeout for user ${id}.`); + console.log(`Timeout duration: ${minutesForTimeout * minute}`); + setTimeout(async () => { + let virtualWinner = await VirtualWinner.findOne({ id: id, prize_selected: false }); + if (!virtualWinner) { + console.log(`Timeout done for user ${id}, but user has already sent data.`); + return; + } + console.log(`Timeout done for user ${id}, sending update to user.`); + + message.sendWineSelectMessageTooLate(virtualWinner); + + virtualWinner.timestamp_drawn = new Date().getTime(); + virtualWinner.timestamp_limit = null; + virtualWinner.timestamp_sent = null; + await virtualWinner.save(); + + notifyNextWinner(); + }, minutesForTimeout * minute); + + return Promise.resolve(); +} + +module.exports = { + verifyWinnerNextInLine, + claimPrize, + notifyNextWinner +}; diff --git a/api/request.js b/api/request.js index 60388fa..01ada71 100644 --- a/api/request.js +++ b/api/request.js @@ -1,41 +1,20 @@ -const express = require("express"); const path = require("path"); -const RequestedWine = require(path.join( - __dirname, "/schemas/RequestedWine" -)); -const Wine = require(path.join( - __dirname, "/schemas/Wine" -)); +const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine")); +const Wine = require(path.join(__dirname, "/schemas/Wine")); -const deleteRequestedWineById = async (req, res) => { - const { id } = req.params; - if(id == null){ - return res.json({ - message: "Id er ikke definert", - success: false - }) +class RequestedWineNotFound extends Error { + constructor(message = "Wine with this id was not found.") { + super(message); + this.name = "RequestedWineNotFound"; + this.statusCode = 404; } - - await RequestedWine.deleteOne({wineId: id}) - return res.json({ - message: `Slettet vin med id: ${id}`, - success: true - }); } -const getAllRequestedWines = async (req, res) => { - const allWines = await RequestedWine.find({}).populate("wine"); +const addNew = async wine => { + let foundWine = await Wine.findOne({ id: wine.id }); - return res.json(allWines); -} - -const requestNewWine = async (req, res) => { - const {wine} = req.body - - let thisWineIsLOKO = await Wine.findOne({id: wine.id}) - - if(thisWineIsLOKO == undefined){ - thisWineIsLOKO = new Wine({ + if (foundWine == undefined) { + foundWine = new Wine({ name: wine.name, vivinoLink: wine.vivinoLink, rating: null, @@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => { image: wine.image, id: wine.id }); - await thisWineIsLOKO.save() + await foundWine.save(); } - let requestedWine = await RequestedWine.findOne({ "wineId": wine.id}) + let requestedWine = await RequestedWine.findOne({ wineId: wine.id }); - if(requestedWine == undefined){ + if (requestedWine == undefined) { requestedWine = new RequestedWine({ count: 1, wineId: wine.id, - wine: thisWineIsLOKO - }) + wine: foundWine + }); } else { requestedWine.count += 1; } - await requestedWine.save() + await requestedWine.save(); - return res.send(requestedWine); -} + return requestedWine; +}; + +const getById = id => { + return RequestedWine.findOne({ wineId: id }) + .populate("wine") + .then(wine => { + if (wine == null) { + throw new RequestedWineNotFound(); + } + + return wine; + }); +}; + +const deleteById = id => { + return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id })); +}; + +const getAll = () => { + return RequestedWine.find({}).populate("wine"); +}; module.exports = { - requestNewWine, - getAllRequestedWines, - deleteRequestedWineById + addNew, + getAll, + deleteById }; diff --git a/api/retrieve.js b/api/retrieve.js deleted file mode 100644 index c29133a..0000000 --- a/api/retrieve.js +++ /dev/null @@ -1,154 +0,0 @@ -const path = require("path"); - -const Purchase = require(path.join(__dirname, "/schemas/Purchase")); -const Wine = require(path.join(__dirname, "/schemas/Wine")); -const Highscore = require(path.join(__dirname, "/schemas/Highscore")); -const PreLotteryWine = require(path.join( - __dirname, "/schemas/PreLotteryWine" -)); - -const prelotteryWines = async (req, res) => { - let wines = await PreLotteryWine.find(); - return res.json(wines); -}; - -const allPurchase = async (req, res) => { - let purchases = await Purchase.find() - .populate("wines") - .sort({ date: 1 }); - return res.json(purchases); -}; - -const purchaseByColor = async (req, res) => { - const countColor = await Purchase.find(); - let red = 0; - let blue = 0; - let yellow = 0; - let green = 0; - let stolen = 0; - for (let i = 0; i < countColor.length; i++) { - let element = countColor[i]; - red += element.red; - blue += element.blue; - yellow += element.yellow; - green += element.green; - if (element.stolen != undefined) { - stolen += element.stolen; - } - } - - const highscore = await Highscore.find(); - let redWin = 0; - let blueWin = 0; - let yellowWin = 0; - let greenWin = 0; - for (let i = 0; i < highscore.length; i++) { - let element = highscore[i]; - for (let y = 0; y < element.wins.length; y++) { - let currentWin = element.wins[y]; - switch (currentWin.color) { - case "blue": - blueWin += 1; - break; - case "red": - redWin += 1; - break; - case "yellow": - yellowWin += 1; - break; - case "green": - greenWin += 1; - break; - } - } - } - - const total = red + yellow + blue + green; - - return res.json({ - red: { - total: red, - win: redWin - }, - blue: { - total: blue, - win: blueWin - }, - green: { - total: green, - win: greenWin - }, - yellow: { - total: yellow, - win: yellowWin - }, - stolen: stolen, - total: total - }); -}; - -const highscore = async (req, res) => { - const highscore = await Highscore.find().populate("wins.wine"); - - return res.json(highscore); -}; - -const allWines = async (req, res) => { - const wines = await Wine.find(); - - return res.json(wines); -}; - -const allWinesSummary = async (req, res) => { - const highscore = await Highscore.find().populate("wins.wine"); - let wines = {}; - - for (let i = 0; i < highscore.length; i++) { - let person = highscore[i]; - for (let y = 0; y < person.wins.length; y++) { - let wine = person.wins[y].wine; - let date = person.wins[y].date; - let color = person.wins[y].color; - - if (wines[wine._id] == undefined) { - wines[wine._id] = { - name: wine.name, - occurences: wine.occurences, - vivinoLink: wine.vivinoLink, - rating: wine.rating, - image: wine.image, - id: wine.id, - _id: wine._id, - dates: [date], - winners: [person.name], - red: 0, - blue: 0, - green: 0, - yellow: 0 - }; - wines[wine._id][color] += 1; - } else { - wines[wine._id].dates.push(date); - wines[wine._id].winners.push(person.name); - if (wines[wine._id][color] == undefined) { - wines[wine._id][color] = 1; - } else { - wines[wine._id][color] += 1; - } - } - } - } - - wines = Object.values(wines).reverse() - - return res.json(wines); -}; - -module.exports = { - prelotteryWines, - allPurchase, - purchaseByColor, - highscore, - allWines, - allWinesSummary -}; diff --git a/api/router.js b/api/router.js index 4d57057..77e8661 100644 --- a/api/router.js +++ b/api/router.js @@ -4,67 +4,97 @@ const path = require("path"); const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated")); const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated")); -const update = require(path.join(__dirname, "/update")); -const retrieve = require(path.join(__dirname, "/retrieve")); -const request = require(path.join(__dirname, "/request")); -const subscriptionApi = require(path.join(__dirname, "/subscriptions")); -const userApi = require(path.join(__dirname, "/user")); -const wineinfo = require(path.join(__dirname, "/wineinfo")); -const virtualApi = require(path.join(__dirname, "/virtualLottery")); -const virtualRegistrationApi = require(path.join( - __dirname, "/virtualRegistration" -)); -const lottery = require(path.join(__dirname, "/lottery")); -const chatHistoryApi = require(path.join(__dirname, "/chatHistory")); +const requestController = require(path.join(__dirname, "/controllers/requestController")); +const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController")); +const chatController = require(path.join(__dirname, "/controllers/chatController")); +const userController = require(path.join(__dirname, "/controllers/userController")); +const historyController = require(path.join(__dirname, "/controllers/historyController")); +const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController")); +const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController")); +const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController")); +const lotteryController = require(path.join(__dirname, "/controllers/lotteryController")); +const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController")); +const wineController = require(path.join(__dirname, "/controllers/wineController")); +const messageController = require(path.join(__dirname, "/controllers/messageController")); const router = express.Router(); -router.get("/wineinfo/search", wineinfo.wineSearch); +router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines); +router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN); +router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById); +router.get("/vinmonopolet/stores/", vinmonopoletController.allStores); +router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores); -router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines); -router.post("/request/new-wine", request.requestNewWine); -router.delete("/request/:id", request.deleteRequestedWineById); +router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests); +router.post("/request", requestController.addRequest); +router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest); -router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); -router.get("/wineinfo/:ean", wineinfo.byEAN); +router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences +router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences +// router.update("/wine/:id", mustBeAuthenticated, wineController.update); -router.post("/log/wines", mustBeAuthenticated, update.submitWines); -router.post("/lottery", update.submitLottery); -router.post("/lottery/wines", update.submitWinesToLottery); -// router.delete("/lottery/wine/:id", update.deleteWineFromLottery); -router.post("/lottery/winners", update.submitWinnersToLottery); +router.get("/history", historyController.all); +router.get("/history/latest", historyController.latest); +router.get("/history/by-wins/", historyController.orderByWins); +router.get("/history/by-color/", historyController.groupByColor); +router.get("/history/by-date/:date", historyController.byDate); +router.get("/history/by-name/:name", historyController.byName); +router.get("/history/search/", historyController.search); +router.get("/history/by-date/", historyController.groupByDate); -router.get("/wines/prelottery", retrieve.prelotteryWines); -router.get("/purchase/statistics", retrieve.allPurchase); -router.get("/purchase/statistics/color", retrieve.purchaseByColor); -router.get("/highscore/statistics", retrieve.highscore) -router.get("/wines/statistics", retrieve.allWines); -router.get("/wines/statistics/overall", retrieve.allWinesSummary); +// router.get("/purchases", purchaseController.lotteryPurchases); +// // returns list per date and count of each colors that where bought +// router.get("/purchases/summary", purchaseController.lotteryPurchases); +// // returns total, wins?, stolen +// router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate); -router.get("/lottery/all", lottery.all); -router.get("/lottery/latest", lottery.latest); -router.get("/lottery/by-date/:date", lottery.byEpochDate); -router.get("/lottery/by-name/:name", lottery.byName); +router.get("/lottery/wines", prelotteryWineController.allWines); +router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema); +router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById); +router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines); +router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines); +router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById); +router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById); -router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners); -router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees); -router.get('/virtual/winner/draw', virtualApi.drawWinner); -router.get('/virtual/winner/all', virtualApi.winners); -router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure); -router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish); -router.get('/virtual/attendee/all', virtualApi.attendees); -router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure); -router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee); +router.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees); +router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees); +router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee); +router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById); +router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById); -router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById); -router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById); -router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection); +router.get("/lottery/winners", winnerController.allWinners); +router.get("/lottery/winner/:id", winnerController.winnerById); +router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners); +router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners); +router.put("/lottery/winner/:id", mustBeAuthenticated, winnerController.updateWinnerById); +router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById); -router.get('/chat/history', chatHistoryApi.getAllHistory) -router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory) +router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner); +router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery); +router.get("/lottery/:epoch", lotteryController.lotteryByDate); +router.get("/lotteries/", lotteryController.allLotteries); -router.post('/login', userApi.login); -router.post('/register', mustBeAuthenticated, userApi.register); -router.get('/logout', userApi.logout); +// router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status); +router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start); +// router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop); +router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById); +router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById); + +router.post("/lottery/messages/winner/:id", mustBeAuthenticated, messageController.notifyWinnerById); + +router.get("/chat/history", chatController.getAllHistory); +router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory); + +router.post("/login", userController.login); +router.post("/register", mustBeAuthenticated, userController.register); +router.get("/logout", userController.logout); + +// router.get("/", documentation.apiInfo); + +// router.get("/wine/schema", mustBeAuthenticated, update.schema); +// router.get("/purchase/statistics", retrieve.allPurchase); +// router.get("/highscore/statistics", retrieve.highscore); +// router.get("/wines/statistics", retrieve.allWines); +// router.get("/wines/statistics/overall", retrieve.allWinesSummary); module.exports = router; diff --git a/api/schemas/PreLotteryWine.js b/api/schemas/PreLotteryWine.js index 69295b5..64f950c 100644 --- a/api/schemas/PreLotteryWine.js +++ b/api/schemas/PreLotteryWine.js @@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({ vivinoLink: String, rating: Number, id: String, + year: Number, image: String, price: String, - country: String + country: String, + winner: { + type: Schema.Types.ObjectId, + ref: "VirtualWinner" + } }); module.exports = mongoose.model("PreLotteryWine", PreLotteryWine); diff --git a/api/schemas/VirtualWinner.js b/api/schemas/VirtualWinner.js index 94f69d1..f18ced1 100644 --- a/api/schemas/VirtualWinner.js +++ b/api/schemas/VirtualWinner.js @@ -10,6 +10,10 @@ const VirtualWinner = new Schema({ red: Number, yellow: Number, id: String, + prize_selected: { + type: Boolean, + default: false + }, timestamp_drawn: Number, timestamp_sent: Number, timestamp_limit: Number diff --git a/api/schemas/Wine.js b/api/schemas/Wine.js index a24fc83..b3c9029 100644 --- a/api/schemas/Wine.js +++ b/api/schemas/Wine.js @@ -1,15 +1,16 @@ const mongoose = require("mongoose"); const Schema = mongoose.Schema; -const Wine = new Schema({ +const WineSchema = new Schema({ name: String, vivinoLink: String, rating: Number, occurences: Number, id: String, + year: Number, image: String, price: String, country: String }); -module.exports = mongoose.model("Wine", Wine); +module.exports = mongoose.model("Wine", WineSchema); diff --git a/api/update.js b/api/update.js deleted file mode 100644 index 88df56a..0000000 --- a/api/update.js +++ /dev/null @@ -1,142 +0,0 @@ -const express = require("express"); -const path = require("path"); - -const sub = require(path.join(__dirname, "/subscriptions")); - -const _wineFunctions = require(path.join(__dirname, "/wine")); -const _personFunctions = require(path.join(__dirname, "/person")); -const Subscription = require(path.join(__dirname, "/schemas/Subscription")); -const Lottery = require(path.join(__dirname, "/schemas/Purchase")); -const PreLotteryWine = require(path.join( - __dirname, "/schemas/PreLotteryWine" -)); - -const submitWines = async (req, res) => { - const wines = req.body; - for (let i = 0; i < wines.length; i++) { - let wine = wines[i]; - let newWonWine = new PreLotteryWine({ - name: wine.name, - vivinoLink: wine.vivinoLink, - rating: wine.rating, - image: wine.image, - price: wine.price, - country: wine.country, - id: wine.id - }); - await newWonWine.save(); - } - - let subs = await Subscription.find(); - console.log("Sending new wines w/ push notification to all subscribers.") - for (let i = 0; i < subs.length; i++) { - let subscription = subs[i]; //get subscription from your databse here. - - const message = JSON.stringify({ - message: "Dagens vin er lagt til, se den på lottis.vin/dagens!", - title: "Ny vin!", - link: "/#/dagens" - }); - - try { - sub.sendNotification(subscription, message); - } catch (error) { - console.error("Error when trying to send push notification to subscriber."); - console.error(error); - } - } - - return res.send({ - message: "Submitted and notified push subscribers of new wines!", - success: true - }); -}; - -const schema = async (req, res) => { - let schema = { ...PreLotteryWine.schema.obj }; - let nulledSchema = Object.keys(schema).reduce((accumulator, current) => { - accumulator[current] = ""; - return accumulator - }, {}); - - return res.send(nulledSchema); -} - -// TODO IMPLEMENT WITH FRONTEND (unused) -const submitWinesToLottery = async (req, res) => { - const { lottery } = req.body; - const { date, wines } = lottery; - const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine))) - - return Lottery.findOneAndUpdate({ date: date }, { - date: date, - wines: wineObjects - }, { - upsert: true - }).then(_ => res.send(true)) - .catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.', - success: false, - exception: err.message })); -} - - /** - * @apiParam (Request body) {Array} winners List of winners - */ -const submitWinnersToLottery = async (req, res) => { - const { lottery } = req.body; - const { winners, date } = lottery; - - for (let i = 0; i < winners.length; i++) { - let currentWinner = winners[i]; - let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery - await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson - } - - return res.json(true); -} - - /** - * @apiParam (Request body) {Date} date Date of lottery - * @apiParam (Request body) {Number} blue Number of blue tickets - * @apiParam (Request body) {Number} red Number of red tickets - * @apiParam (Request body) {Number} green Number of green tickets - * @apiParam (Request body) {Number} yellow Number of yellow tickets - * @apiParam (Request body) {Number} bought Number of tickets bought - * @apiParam (Request body) {Number} stolen Number of tickets stolen - */ -const submitLottery = async (req, res) => { - const { lottery } = req.body - - const { date, - blue, - red, - yellow, - green, - bought, - stolen } = lottery; - - return Lottery.findOneAndUpdate({ date: date }, { - date: date, - blue: blue, - yellow: yellow, - red: red, - green: green, - bought: bought, - stolen: stolen - }, { - upsert: true - }).then(_ => res.send(true)) - .catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.', - success: false, - exception: err.message })); - - return res.send(true); -}; - -module.exports = { - submitWines, - schema, - submitLottery, - submitWinnersToLottery, - submitWinesToLottery -}; diff --git a/api/user.js b/api/user.js index 7a2ce9e..34c7af4 100644 --- a/api/user.js +++ b/api/user.js @@ -1,51 +1,90 @@ const passport = require("passport"); const path = require("path"); const User = require(path.join(__dirname, "/schemas/User")); -const router = require("express").Router(); -const register = (req, res, next) => { - User.register( - new User({ username: req.body.username }), - req.body.password, - function(err) { +class UserExistsError extends Error { + constructor(message = "Username already exists.") { + super(message); + this.name = "UserExists"; + this.statusCode = 409; + } +} + +class MissingUsernameError extends Error { + constructor(message = "No username given.") { + super(message); + this.name = "MissingUsernameError"; + this.statusCode = 400; + } +} + +class MissingPasswordError extends Error { + constructor(message = "No password given.") { + super(message); + this.name = "MissingPasswordError"; + this.statusCode = 400; + } +} + +class IncorrectUserCredentialsError extends Error { + constructor(message = "Incorrect username or password") { + super(message); + this.name = "IncorrectUserCredentialsError"; + this.statusCode = 404; + } +} + +function userAuthenticationErrorHandler(err) { + if (err.name == "UserExistsError") { + throw new UserExistsError(err.message); + } else if (err.name == "MissingUsernameError") { + throw new MissingUsernameError(err.message); + } else if (err.name == "MissingPasswordError") { + throw new MissingPasswordError(err.message); + } + + throw err; +} + +const register = (username, password) => { + return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler); +}; + +const authenticate = req => { + return new Promise((resolve, reject) => { + const { username, password } = req.body; + + if (username == undefined) throw new MissingUsernameError(); + if (password == undefined) throw new MissingPasswordError(); + + passport.authenticate("local", function(err, user, info) { if (err) { - if (err.name == "UserExistsError") - res.status(409).send({ success: false, message: err.message }) - else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") - res.status(400).send({ success: false, message: err.message }) - return next(err); + reject(err); } - return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true }) - } - ); + if (!user) { + reject(new IncorrectUserCredentialsError()); + } + + resolve(user); + })(req); + }); }; -const login = (req, res, next) => { - passport.authenticate("local", function(err, user, info) { - if (err) { - if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") - return res.status(400).send({ message: err.message, success: false }) - return next(err); - } +const login = (req, user) => { + return new Promise((resolve, reject) => { + req.logIn(user, err => { + if (err) { + reject(err); + } - if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false }) - - req.logIn(user, (err) => { - if (err) { return next(err) } - - return res.status(200).send({ message: "Velkommen " + user.username, success: true }) - }) - })(req, res, next); -}; - -const logout = (req, res) => { - req.logout(); - res.redirect("/"); + resolve(user); + }); + }); }; module.exports = { register, - login, - logout + authenticate, + login }; diff --git a/api/vinlottisErrors.js b/api/vinlottisErrors.js new file mode 100644 index 0000000..d812627 --- /dev/null +++ b/api/vinlottisErrors.js @@ -0,0 +1,90 @@ +class UserNotFound extends Error { + constructor(message = "User not found.") { + super(message); + this.name = "UserNotFound"; + this.statusCode = 404; + } + + // TODO log missing user +} + +class WineNotFound extends Error { + constructor(message = "Wine not found.") { + super(message); + this.name = "WineNotFound"; + this.statusCode = 404; + } + + // 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 WinnersTimelimitExpired extends Error { + constructor(message = "Timelimit expired, you will need to wait until it's your turn again.") { + super(message); + this.name = "WinnersTimelimitExpired"; + this.statusCode = 403; + } +} + +class WineSelectionWinnerNotNextInLine extends Error { + constructor(message = "Not the winner next in line!") { + super(message); + this.name = "WineSelectionWinnerNotNextInLine"; + this.statusCode = 403; + } + + // TODO log missing user +} + +class NoMoreAttendeesToWin extends Error { + constructor(message = "No more attendees left to drawn from.") { + super(message); + this.name = "NoMoreAttendeesToWin"; + this.statusCode = 404; + } +} + +class CouldNotFindNewWinnerAfterNTries extends Error { + constructor(tries) { + let message = `Could not a new winner after ${tries} tries.`; + super(message); + this.name = "CouldNotFindNewWinnerAfterNTries"; + this.statusCode = 404; + } +} + +class LotteryByDateNotFound extends Error { + constructor(date) { + const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); + const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); + const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); + + const dateString = `${ye}-${mo}-${da}`; + const dateUnix = date.getTime(); + const message = `Could not find lottery for date: ${dateString}.`; + super(message); + this.name = "LotteryByDateNotFoundError"; + this.statusCode = 404; + } +} + +module.exports = { + UserNotFound, + WineNotFound, + WinnerNotFound, + WinnersTimelimitExpired, + WineSelectionWinnerNotNextInLine, + NoMoreAttendeesToWin, + CouldNotFindNewWinnerAfterNTries, + LotteryByDateNotFound +}; diff --git a/api/vinmonopolet.js b/api/vinmonopolet.js new file mode 100644 index 0000000..164e09f --- /dev/null +++ b/api/vinmonopolet.js @@ -0,0 +1,114 @@ +const fetch = require("node-fetch"); +const path = require("path"); +const config = require(path.join(__dirname + "/../config/env/lottery.config")); + +const convertToOurWineObject = wine => { + if (wine.basic.ageLimit === "18") { + return { + name: wine.basic.productShortName, + vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId, + rating: wine.basic.alcoholContent, + occurences: 0, + id: wine.basic.productId, + year: wine.basic.vintage, + image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`, + price: wine.prices[0].salesPrice.toString(), + country: wine.origins.origin.country + }; + } +}; + +const convertToOurStoreObject = store => { + return { + id: store.storeId, + name: store.storeName, + ...store.address + }; +}; + +const searchWinesByName = async (name, page = 1) => { + const pageSize = 15; + let url = new URL( + `https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15` + ); + url.searchParams.set("maxResults", pageSize); + url.searchParams.set("start", pageSize * (page - 1)); + url.searchParams.set("productShortNameContains", name); + + const vinmonopoletResponse = await fetch(url, { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }) + .then(resp => resp.json()) + .catch(err => console.error(err)); + + if (vinmonopoletResponse.errors != null) { + return vinmonopoletResponse.errors.map(error => { + if (error.type == "UnknownProductError") { + return res.status(404).json({ + message: error.message + }); + } else { + return next(); + } + }); + } + const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean); + + return winesConverted; +}; + +const wineByEAN = ean => { + const url = `https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/${ean}`; + return fetch(url) + .then(resp => resp.json()) + .then(response => response.map(convertToOurWineObject)); +}; + +const wineById = id => { + const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`; + const options = { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }; + + return fetch(url, options) + .then(resp => resp.json()) + .then(response => response.map(convertToOurWineObject)); +}; + +const allStores = () => { + const url = `https://apis.vinmonopolet.no/stores/v0/details`; + const options = { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }; + + return fetch(url, options) + .then(resp => resp.json()) + .then(response => response.map(convertToOurStoreObject)); +}; + +const searchStoresByName = name => { + const url = `https://apis.vinmonopolet.no/stores/v0/details?storeNameContains=${name}`; + const options = { + headers: { + "Ocp-Apim-Subscription-Key": config.vinmonopoletToken + } + }; + + return fetch(url, options) + .then(resp => resp.json()) + .then(response => response.map(convertToOurStoreObject)); +}; + +module.exports = { + searchWinesByName, + wineByEAN, + wineById, + allStores, + searchStoresByName +}; diff --git a/api/virtualLottery.js b/api/virtualLottery.js deleted file mode 100644 index 706016b..0000000 --- a/api/virtualLottery.js +++ /dev/null @@ -1,281 +0,0 @@ -const path = require("path"); -const crypto = require("crypto"); - -const config = require(path.join(__dirname, "/../config/defaults/lottery")); -const Message = require(path.join(__dirname, "/message")); -const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration")); - -const Attendee = require(path.join(__dirname, "/schemas/Attendee")); -const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); -const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); - - -const winners = async (req, res) => { - let winners = await VirtualWinner.find(); - let winnersRedacted = []; - let winner; - for (let i = 0; i < winners.length; i++) { - winner = winners[i]; - winnersRedacted.push({ - name: winner.name, - color: winner.color - }); - } - res.json(winnersRedacted); -}; - -const winnersSecure = async (req, res) => { - let winners = await VirtualWinner.find(); - - return res.json(winners); -}; - -const deleteWinners = async (req, res) => { - await VirtualWinner.deleteMany(); - var io = req.app.get('socketio'); - io.emit("refresh_data", {}); - return res.json(true); -}; - -const attendees = async (req, res) => { - let attendees = await Attendee.find(); - let attendeesRedacted = []; - let attendee; - for (let i = 0; i < attendees.length; i++) { - attendee = attendees[i]; - attendeesRedacted.push({ - name: attendee.name, - raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, - red: attendee.red, - blue: attendee.blue, - green: attendee.green, - yellow: attendee.yellow - }); - } - return res.json(attendeesRedacted); -}; - -const attendeesSecure = async (req, res) => { - let attendees = await Attendee.find(); - - return res.json(attendees); -}; - -const addAttendee = async (req, res) => { - const attendee = req.body; - const { red, blue, yellow, green } = attendee; - - let newAttendee = new Attendee({ - name: attendee.name, - red, - blue, - green, - yellow, - phoneNumber: attendee.phoneNumber, - winner: false - }); - await newAttendee.save(); - - - var io = req.app.get('socketio'); - io.emit("new_attendee", {}); - - return res.send(true); -}; - -const deleteAttendees = async (req, res) => { - await Attendee.deleteMany(); - var io = req.app.get('socketio'); - io.emit("refresh_data", {}); - return res.json(true); -}; - -const drawWinner = async (req, res) => { - let allContestants = await Attendee.find({ winner: false }); - - if (allContestants.length == 0) { - return res.json({ - success: false, - message: "No attendees left that have not won." - }); - } - 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) { - return res.status(404).send({ - success: false, - message: `Klarte ikke trekke en vinner etter ${maxTries} forsøk.` - }); - } - - 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 - }); - - var io = req.app.get('socketio'); - io.emit("winner", { - color: colorToChooseFrom, - name: winner.name, - winner_count: winners.length + 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 Attendee.update( - { name: winner.name, phoneNumber: winner.phoneNumber }, - { $set: { winner: true } } - ); - - await newWinnerElement.save(); - return res.json({ - success: true, - winner - }); -}; - -const finish = async (req, res) => { - if (!config.gatewayToken) { - return res.json({ - message: "Missing api token for sms gateway.", - success: false - }); - } - - let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ - timestamp_drawn: 1 - }); - - if (winners.length == 0) { - return res.json({ - message: "No winners to draw from.", - success: false - }); - } - - Message.sendInitialMessageToWinners(winners.slice(1)); - - return findAndNotifyNextWinner() - .then(() => res.json({ - success: true, - message: "Sent wine select message to first winner and update message to rest of winners." - })) - .catch(error => res.json({ - message: error["message"] || "Unable to send message to first winner.", - success: false - })) -}; - -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 = { - deleteWinners, - deleteAttendees, - winners, - winnersSecure, - drawWinner, - finish, - attendees, - attendeesSecure, - addAttendee -} - diff --git a/api/virtualRegistration.js b/api/virtualRegistration.js deleted file mode 100644 index ec869a3..0000000 --- a/api/virtualRegistration.js +++ /dev/null @@ -1,200 +0,0 @@ -const path = require("path"); - -const _wineFunctions = require(path.join(__dirname, "/wine")); -const _personFunctions = require(path.join(__dirname, "/person")); -const Message = require(path.join(__dirname, "/message")); -const VirtualWinner = require(path.join( - __dirname, "/schemas/VirtualWinner" -)); -const PreLotteryWine = require(path.join( - __dirname, "/schemas/PreLotteryWine" -)); - - -const getWinesToWinnerById = async (req, res) => { - let id = req.params.id; - let foundWinner = await VirtualWinner.findOne({ id: id }); - - if (!foundWinner) { - return res.json({ - success: false, - message: "No winner with this id.", - existing: false, - turn: false - }); - } - - let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); - if ( - allWinners[0].id != foundWinner.id || - foundWinner.timestamp_limit == undefined || - foundWinner.timestamp_sent == undefined - ) { - return res.json({ - success: false, - message: "Not the winner next in line!", - existing: true, - turn: false - }); - } - - return res.json({ - success: true, - existing: true, - turn: true, - name: foundWinner.name, - color: foundWinner.color - }); -}; - -const registerWinnerSelection = async (req, res) => { - let id = req.params.id; - let wineName = req.body.wineName; - let foundWinner = await VirtualWinner.findOne({ id: id }); - - if (!foundWinner) { - return res.json({ - success: false, - message: "No winner with this id." - }) - } else if (foundWinner.timestamp_limit < new Date().getTime()) { - return res.json({ - success: false, - message: "Timelimit expired, you will receive a wine after other users have chosen.", - limit: true - }) - } - - let date = new Date(); - date.setHours(5, 0, 0, 0); - let prelotteryWine = await PreLotteryWine.findOne({ name: wineName }); - - if (!prelotteryWine) { - return res.json({ - success: false, - message: "No wine with this name.", - wine: false - }); - } - - let wonWine = await _wineFunctions.findSaveWine(prelotteryWine); - await prelotteryWine.delete(); - await _personFunctions.findSavePerson(foundWinner, wonWine, date); - await Message.sendWineConfirmation(foundWinner, wonWine, date); - - await foundWinner.delete(); - console.info("Saved winners choice."); - - return findAndNotifyNextWinner() - .then(() => res.json({ - message: "Choice saved and next in line notified.", - success: true - })) - .catch(error => res.json({ - message: error["message"] || "Error when notifing next winner.", - success: false - })) -}; - -const chooseLastWineForUser = (winner, preLotteryWine) => { - let date = new Date(); - date.setHours(5, 0, 0, 0); - - return _wineFunctions.findSaveWine(preLotteryWine) - .then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date)) - .then(() => preLotteryWine.delete()) - .then(() => Message.sendLastWinnerMessage(winner, preLotteryWine)) - .then(() => winner.delete()) - .catch(err => { - console.log("Error thrown from chooseLastWineForUser: " + err); - throw err; - }) -} - -const findAndNotifyNextWinner = async () => { - let nextWinner = undefined; - - let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); - let winesLeft = await PreLotteryWine.find(); - - if (winnersLeft.length > 1) { - console.log("multiple winners left, choose next in line") - nextWinner = winnersLeft[0]; // multiple winners left, choose next in line - } else if (winnersLeft.length == 1 && winesLeft.length > 1) { - console.log("one winner left, but multiple wines") - nextWinner = winnersLeft[0] // one winner left, but multiple wines - } else if (winnersLeft.length == 1 && winesLeft.length == 1) { - console.log("one winner and one wine left, choose for user") - nextWinner = winnersLeft[0] // one winner and one wine left, choose for user - wine = winesLeft[0] - return chooseLastWineForUser(nextWinner, wine); - } - - if (nextWinner) { - return Message.sendWineSelectMessage(nextWinner) - .then(messageResponse => startTimeout(nextWinner.id)) - } else { - console.info("All winners notified. Could start cleanup here."); - return Promise.resolve({ - message: "All winners notified." - }) - } -}; - -const sendNotificationToWinnerById = async (req, res) => { - const { id } = req.params; - let winner = await VirtualWinner.findOne({ id: id }); - - if (!winner) { - return res.json({ - message: "No winner with this id.", - success: false - }) - } - - return Message.sendWineSelectMessage(winner) - .then(success => res.json({ - success: success, - message: `Message sent to winner ${id} successfully!` - })) - .catch(err => res.json({ - success: false, - message: "Error while trying to send sms.", - error: err - })) -} - -function startTimeout(id) { - const minute = 60000; - const minutesForTimeout = 10; - - console.log(`Starting timeout for user ${id}.`); - console.log(`Timeout duration: ${ minutesForTimeout * minute }`) - setTimeout(async () => { - let virtualWinner = await VirtualWinner.findOne({ id: id }); - if (!virtualWinner) { - console.log(`Timeout done for user ${id}, but user has already sent data.`); - return; - } - console.log(`Timeout done for user ${id}, sending update to user.`); - - Message.sendWineSelectMessageTooLate(virtualWinner); - - virtualWinner.timestamp_drawn = new Date().getTime(); - virtualWinner.timestamp_limit = null; - virtualWinner.timestamp_sent = null; - await virtualWinner.save(); - - findAndNotifyNextWinner(); - }, minutesForTimeout * minute); - - return Promise.resolve() -} - -module.exports = { - getWinesToWinnerById, - registerWinnerSelection, - findAndNotifyNextWinner, - - sendNotificationToWinnerById -}; diff --git a/api/wine.js b/api/wine.js index d953013..cd35bb4 100644 --- a/api/wine.js +++ b/api/wine.js @@ -1,27 +1,63 @@ const path = require("path"); const Wine = require(path.join(__dirname, "/schemas/Wine")); -async function findSaveWine(prelotteryWine) { - let wonWine = await Wine.findOne({ name: prelotteryWine.name }); - if (wonWine == undefined) { - let newWonWine = new Wine({ - name: prelotteryWine.name, - vivinoLink: prelotteryWine.vivinoLink, - rating: prelotteryWine.rating, +const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const addWine = async wine => { + let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }); + + if (existingWine == undefined) { + let newWine = new Wine({ + name: wine.name, + vivinoLink: wine.vivinoLink, + rating: wine.rating, occurences: 1, - image: prelotteryWine.image, - id: prelotteryWine.id + id: wine.id, + year: wine.year, + image: wine.image, + price: wine.price, + country: wine.country }); - await newWonWine.save(); - wonWine = newWonWine; + await newWine.save(); + return newWine; } else { - wonWine.occurences += 1; - wonWine.image = prelotteryWine.image; - wonWine.id = prelotteryWine.id; - await wonWine.save(); + existingWine.occurences += 1; + await existingWine.save(); + return existingWine; } +}; - return wonWine; -} +const allWines = (limit = undefined) => { + if (limit) { + return Wine.find().limit(limit); + } else { + return Wine.find(); + } +}; -module.exports.findSaveWine = findSaveWine; +const wineById = id => { + return Wine.findOne({ _id: id }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return wine; + }); +}; + +const findWine = wine => { + return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => { + if (wine == null) { + throw new WineNotFound(); + } + + return wine; + }); +}; + +module.exports = { + addWine, + allWines, + wineById, + findWine +}; diff --git a/api/wineinfo.js b/api/wineinfo.js deleted file mode 100644 index e52039f..0000000 --- a/api/wineinfo.js +++ /dev/null @@ -1,72 +0,0 @@ -const fetch = require('node-fetch') -const path = require('path') -const config = require(path.join(__dirname + "/../config/env/lottery.config")); - -const convertToOurWineObject = wine => { - if(wine.basic.ageLimit === "18"){ - return { - name: wine.basic.productShortName, - vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId, - rating: wine.basic.alcoholContent, - occurences: 0, - id: wine.basic.productId, - image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`, - price: wine.prices[0].salesPrice.toString(), - country: wine.origins.origin.country - } - } -} - -const wineSearch = async (req, res) => { - const {query} = req.query - let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`) - url.searchParams.set('productShortNameContains', query) - - const vinmonopoletResponse = await fetch(url, { - headers: { - "Ocp-Apim-Subscription-Key": config.vinmonopoletToken - } - }) - .then(resp => resp.json()) - .catch(err => console.error(err)) - - - if (vinmonopoletResponse.errors != null) { - return vinmonopoletResponse.errors.map(error => { - if (error.type == "UnknownProductError") { - return res.status(404).json({ - message: error.message - }) - } else { - return next() - } - }) - } - const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean) - - return res.send(winesConverted); -} - -const byEAN = async (req, res) => { - const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean) - .then(resp => resp.json()) - - if (vinmonopoletResponse.errors != null) { - return vinmonopoletResponse.errors.map(error => { - if (error.type == "UnknownProductError") { - return res.status(404).json({ - message: error.message - }) - } else { - return next() - } - }) - } - - return res.send(vinmonopoletResponse); -}; - -module.exports = { - byEAN, - wineSearch -}; diff --git a/api/winner.js b/api/winner.js new file mode 100644 index 0000000..85514ed --- /dev/null +++ b/api/winner.js @@ -0,0 +1,95 @@ +const path = require("path"); + +const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); +const { WinnerNotFound } = require(path.join(__dirname, "/vinlottisErrors")); + +const redactWinnerInfoMapper = winner => { + return { + name: winner.name, + color: winner.color + }; +}; + +const addWinners = winners => { + return Promise.all( + winners.map(winner => { + let newWinnerElement = new VirtualWinner({ + name: winner.name, + color: winner.color, + timestamp_drawn: new Date().getTime() + }); + + return newWinnerElement.save(); + }) + ); +}; + +const allWinners = (isAdmin = false) => { + const sortQuery = { timestamp_drawn: 1 }; + + if (!isAdmin) { + return VirtualWinner.find() + .sort(sortQuery) + .then(winners => winners.map(redactWinnerInfoMapper)); + } else { + return VirtualWinner.find().sort(sortQuery); + } +}; + +const winnerById = (id, isAdmin = false) => { + return VirtualWinner.findOne({ id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + if (!isAdmin) { + return redactWinnerInfoMapper(winner); + } + return winner; + }); +}; + +const updateWinnerById = (id, updateModel) => { + return VirtualWinner.findOne({ id: id }).then(winner => { + if (winner == null) { + throw new WinnerNotFound(); + } + + const updatedWinner = { + name: updateModel.name != null ? updateModel.name : winner.name, + phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : winner.phoneNumber, + red: updateModel.red != null ? updateModel.red : winner.red, + green: updateModel.green != null ? updateModel.green : winner.green, + blue: updateModel.blue != null ? updateModel.blue : winner.blue, + yellow: updateModel.yellow != null ? updateModel.yellow : winner.yellow, + timestamp_drawn: updateModel.timestamp_drawn != null ? updateModel.timestamp_drawn : winner.timestamp_drawn, + timestamp_limit: updateModel.timestamp_limit != null ? updateModel.timestamp_limit : winner.timestamp_limit, + timestamp_sent: updateModel.timestamp_sent != null ? updateModel.timestamp_sent : winner.timestamp_sent + }; + + return VirtualWinner.updateOne({ id: id }, updatedWinner).then(_ => updatedWinner); + }); +}; + +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(); +}; + +module.exports = { + addWinners, + allWinners, + winnerById, + updateWinnerById, + deleteWinnerById, + deleteWinners +}; diff --git a/frontend/api.js b/frontend/api.js index ca17f6a..d3526aa 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -26,7 +26,8 @@ const allRequestedWines = () => {; return fetch("/api/request/all") .then(resp => { const isAdmin = resp.headers.get("vinlottis-admin") == "true"; - return Promise.all([resp.json(), isAdmin]); + const getWinesFromBody = (resp) => resp.json().then(body => body.wines); + return Promise.all([getWinesFromBody(resp), isAdmin]); }); }; @@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => { headers: { "Content-Type": "application/json" }, - method: "DELETE", - body: JSON.stringify(wineToBeDeleted) + method: "DELETE" }; return fetch("api/request/" + wineToBeDeleted.id, options) @@ -148,14 +148,12 @@ const attendees = () => { const requestNewWine = (wine) => { const options = { - body: JSON.stringify({ - wine: wine - }), + method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - method: "post" + body: JSON.stringify({ wine }) } return fetch("/api/request/new-wine", options) diff --git a/frontend/components/AdminPage.vue b/frontend/components/AdminPage.vue index db9d31b..7c76d77 100644 --- a/frontend/components/AdminPage.vue +++ b/frontend/components/AdminPage.vue @@ -1,34 +1,72 @@ - diff --git a/frontend/components/AllRequestedWines.vue b/frontend/components/AllRequestedWines.vue index a2996d1..8dbabd7 100644 --- a/frontend/components/AllRequestedWines.vue +++ b/frontend/components/AllRequestedWines.vue @@ -2,40 +2,50 @@

Alle foreslåtte viner

-
+

Ingen har foreslått noe enda!

- +
\ No newline at end of file + diff --git a/frontend/components/AllWinesPage.vue b/frontend/components/AllWinesPage.vue index 57a65bf..3bb821f 100644 --- a/frontend/components/AllWinesPage.vue +++ b/frontend/components/AllWinesPage.vue @@ -2,10 +2,9 @@

Alle viner

-
+
- Vinnende lodd:
{{ wine.blue == null ? 0 : wine.blue }} @@ -19,43 +18,44 @@
  • {{ winner }} - -  - {{ dateString(wine.dates[index]) }} + -  + {{ + dateString(wine.dates[index]) + }}
-
@@ -84,18 +84,6 @@ h1 { font-weight: 600; } -#wines-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - grid-gap: 2rem; - - - > div { - justify-content: flex-start; - margin-bottom: 2rem; - } -} - .name-wins { display: flex; flex-direction: column; diff --git a/frontend/components/GeneratePage.vue b/frontend/components/GeneratePage.vue index 80996d2..4e9f638 100644 --- a/frontend/components/GeneratePage.vue +++ b/frontend/components/GeneratePage.vue @@ -5,7 +5,7 @@ Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'

- + @@ -43,16 +43,16 @@ export default { this.hardStart = true; }, track() { - window.ga('send', 'pageview', '/lottery/generate'); + window.ga("send", "pageview", "/lottery/generate"); } } }; \ No newline at end of file + diff --git a/frontend/components/LoginPage.vue b/frontend/components/LoginPage.vue index 3a13d4d..fc37a70 100644 --- a/frontend/components/LoginPage.vue +++ b/frontend/components/LoginPage.vue @@ -7,6 +7,7 @@ Vinnende farger:
-
+
{{ occurences }}

Flasker vunnet:

-
+
- {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden + {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden - -
- + +
+

{{ win.wine.name }}

@@ -38,6 +43,11 @@
+
+
+

Oisann! Klarte ikke finne vin.

+
+
@@ -49,67 +59,71 @@ \ No newline at end of file + diff --git a/frontend/components/RegisterPage.vue b/frontend/components/RegisterPage.vue deleted file mode 100644 index 2709b2a..0000000 --- a/frontend/components/RegisterPage.vue +++ /dev/null @@ -1,728 +0,0 @@ -