synt_backend
Version:
Synt light-weight node backend service
724 lines (673 loc) • 19.9 kB
JavaScript
import { Router } from "express";
const router = Router();
const db = require("./../mysql/models/index");
import * as ValidationHelper from "./../helpers/validations";
const userHelper = require("./../helpers/user");
const Sequelize = require("sequelize");
const notifier = require("./../helpers/notifier");
import {
formatMeetingItem,
formatPowerOfAttorneys,
findCommissioners,
setMeetingItemResults,
} from "../helpers/format";
import { jobReminderManager } from "../helpers/JobReminderManager";
router.get("/:MeetingId", getMeeting);
router.get("/", getMeetings);
router.post("/powerofattorney", getMeetingWithPowerOfAttorney);
router.post("/postponed", postPostponedMeeting);
router.post("/", postMeeting);
router.post("/:MeetingId/item", postMeetingItem);
router.post("/:MeetingId/stream", toggleStreamMeeting);
router.post("/:MeetingId/user/:UserId", toggleUserPresenceConfirmation);
router.post("/:MeetingId/left/:UserId", toggleUserLeft);
router.post("/:MeetingId/powerofattorney", setPowerOfAttorney);
router.post("/:MeetingId/presence", toggleUserPresence);
async function getMeeting(req, res) {
const { t } = req;
// verify vme and get vme
let user = await userHelper.getAuthUser(req);
let VmeValidation = await ValidationHelper.validateVme(t, user.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const { MeetingId } = req.params;
db.Meeting.findOne({
where: { id: MeetingId },
include: [
{
model: db.MeetingItem,
include: [
{ model: db.FixedMeetingItem },
{ model: db.User, as: "Users" },
],
},
{
model: db.User,
as: "Users",
},
{ model: db.FinancialYear },
],
})
.then(async (Meeting) => {
if (Meeting.VMEId !== VME.id) {
return res.json({
success: false,
error: t(
"api.meetings.errors.notYourVme",
"This event is not part of your VME."
),
});
} else if (Meeting) {
const Users = await VME.getUsers();
// FIXME: include pivot through: how? (like above)
Meeting = formatPowerOfAttorneys(Meeting, Users);
Meeting.MeetingItems = Meeting.MeetingItems.map((item) =>
formatMeetingItem(item)
);
// is rechtsgeldig
Meeting = await formatLegalValidityMeeting(VME, Meeting);
return res.json({
success: true,
Meeting,
});
} else {
return res.json({
success: false,
error: t(
"api.meetings.errors.eventNotExists",
"This event does not exist. Contact support"
),
});
}
})
.catch((err) => console.log(err));
}
async function getMeetingWithPowerOfAttorney(req, res) {
const { t } = req;
const { first_name, last_name, token } = req.body;
if (!first_name) {
return res.json({
success: false,
errors: {
first_name: t(
"api.meetings.errors.firstNameRequired",
"First name is required."
),
},
});
}
if (!last_name) {
return res.json({
success: false,
errors: {
last_name: t(
"api.meetings.errors.lastNameRequired",
"Last name is required."
),
},
});
}
if (!token) {
return res.json({
success: false,
errors: {
token: t("api.meetings.errors.tokenRequired", "Token is required."),
},
});
}
db.MeetingUser.findOne({
where: { power_of_attorney_token: token },
}).then((MeetingUser) => {
if (first_name !== MeetingUser.power_of_attorney_given_to_first_name) {
return res.json({
success: false,
errors: {
first_name: t(
"api.meetings.errors.firstNameIncorrect",
"First name is incorrect. Check spelling."
),
},
});
}
if (last_name !== MeetingUser.power_of_attorney_given_to_last_name) {
return res.json({
success: false,
errors: {
last_name: t(
"api.meetings.errors.lastNameIncorrect",
"Last name is incorrect. Check spelling."
),
},
});
}
db.Meeting.findOne({
where: { id: MeetingUser.MeetingId },
include: [
{
model: db.MeetingItem,
include: [
{ model: db.FixedMeetingItem },
{ model: db.User, as: "Users" },
],
},
{
model: db.User,
as: "Users",
},
{
model: db.FinancialYear,
},
],
})
.then((Meeting) => {
if (Meeting) {
Meeting.MeetingItems = Meeting.MeetingItems.map((item) =>
formatMeetingItem(item)
);
return res.json({
success: true,
Meeting,
User: Meeting.Users.reduce((acc, item) => {
if (item.id === MeetingUser.UserId) {
return item;
}
return acc;
}, {}),
});
} else {
return res.json({
success: false,
error: t(
"api.meetings.errors.eventNotExists",
"This event does not exist. Contact support"
),
});
}
})
.catch((err) => console.log(err));
});
}
async function toggleUserPresence(req, res) {
const { t } = req;
let UserId;
if (req.body.UserId) {
// public flow
UserId = req.body.UserId;
} else {
let User = await userHelper.getAuthUser(req);
UserId = User.id || false;
}
if (UserId) {
const { MeetingId } = req.params;
db.MeetingUser.findOrCreate({ where: { MeetingId, UserId } })
.then((res) => console.log(res))
.catch((err) => console.log(err));
return res.json({ success: true });
} else {
return res.json({
success: false,
error: t("api.meetings.errors.noPermission", "No permission."),
});
}
}
async function setPowerOfAttorney(req, res) {
const { MeetingId } = req.params;
let User = await userHelper.getAuthUser(req);
if (User) {
const {
has_power_of_attorney,
PowerOfAttorneyGivenToUserId,
power_of_attorney_given_to_first_name,
power_of_attorney_given_to_last_name,
} = req.body;
db.MeetingUser.findOrCreate({ where: { MeetingId, UserId: User.id } }).then(
(MeetingUser) => {
MeetingUser = MeetingUser[0];
MeetingUser.update({
has_power_of_attorney,
power_of_attorney_given_to_first_name: has_power_of_attorney
? power_of_attorney_given_to_first_name
: null,
power_of_attorney_given_to_last_name: has_power_of_attorney
? power_of_attorney_given_to_last_name
: null,
PowerOfAttorneyGivenToUserId: has_power_of_attorney
? PowerOfAttorneyGivenToUserId
: null,
});
return res.json({
success: true,
power_of_attorney_given_to_first_name:
MeetingUser.power_of_attorney_given_to_first_name,
power_of_attorney_given_to_last_name:
MeetingUser.power_of_attorney_given_to_last_name,
power_of_attorney_token: MeetingUser.power_of_attorney_token,
});
}
);
}
}
async function toggleUserPresenceConfirmation(req, res) {
const { MeetingId, UserId } = req.params;
db.MeetingUser.update(
{ is_confirmed_at: new Date() },
{ where: { MeetingId, UserId } }
);
return res.json({ success: true });
}
async function toggleUserLeft(req, res) {
const { MeetingId, UserId } = req.params;
db.MeetingUser.update(
{ has_left_at: new Date() },
{ where: { MeetingId, UserId } }
);
return res.json({ success: true });
}
async function toggleStreamMeeting(req, res) {
//TODO: Safety
const { MeetingId } = req.params;
db.Meeting.update(
{ is_streaming: Sequelize.literal("NOT is_streaming") },
{ where: { id: MeetingId } }
);
return res.json({ success: true });
}
async function postPostponedMeeting(req, res) {
const { t } = req;
try {
let User = await userHelper.getAuthUser(req);
if (User) {
// verify vme and get vme
let VmeValidation = await ValidationHelper.validateVme(t, User.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
const { starts_at, ends_at, Meeting } = req.body;
let errors = {};
let now = new Date();
if (Meeting.MeetingId) {
// selected date is in the past
errors["starts_at"] = t(
"api.meetings.errors.meetingAlreadyStarted",
"The meeting has already been restarted."
);
}
if (!starts_at) {
// selected date is in the past
errors["starts_at"] = t(
"api.meetings.errors.startTimeRequired",
"Start time is required."
);
}
if (new Date(starts_at) < now) {
// selected date is in the past
errors["starts_at"] = t(
"api.meetings.errors.startTimeInPast",
"Start date is in the past."
);
}
if (new Date(ends_at) < now) {
// selected date is in the past
errors["ends_at"] = t(
"api.meetings.errors.endTimeInPast",
"End date is in the past."
);
}
if (new Date(ends_at) < starts_at) {
// selected date is in the past
errors["ends_at"] = t(
"api.meetings.errors.startTimeAfterEndTime",
"End date must be after the start date."
);
}
// return errors if there are any
if (Object.keys(errors).length) {
return res.json({
success: false,
errors,
});
}
// save new postponed meeting
createMeeting(
VME,
{
...Meeting,
id: null, // prevent unique constraint error
starts_at,
ends_at,
MeetingId: Meeting.id,
name:
t("api.meetings.meetingNamePrefix", "(Renewed)") +
" " +
Meeting.name,
description:
t(
"api.meetings.meetingDescriptionPrefix",
"This is a renewed meeting and thus the last chance to participate."
) +
" " +
Meeting.description,
is_streaming: false,
createdAt: new Date(),
updatedAt: new Date(),
},
Meeting.MeetingItems.map((Item) => ({ ...Item, id: null })),
User
);
return res.json({ success: true });
}
} catch (error) {
return res.json({ success: false, error });
}
}
async function postMeetingItem(req, res) {
const { t } = req;
let user = await userHelper.getAuthUser(req);
if (user) {
const {
name,
description,
decision,
is_vote_required,
majority,
is_accepted,
} = req.body;
const { MeetingId } = req.params; // TODO: check user whether he/she has access to the meeting and permissions
const meeting_item = {
name,
description,
is_vote_required,
is_accepted,
majority,
decision,
UserId: user.id, // save which user is adding the item
MeetingId,
};
if (!MeetingId) {
return res.json({
success: false,
error: t(
"api.meetings.errors.agendaNotLinkedToMeeting",
"Agenda item must be linked to a meeting. Contact administrator."
),
});
}
if (!name || name === "") {
return res.json({
success: false,
errors: {
name: t("api.meetings.errors.nameRequired", "Name is required."),
},
});
}
db.MeetingItem.create(meeting_item).then(() => {
return res.json({ success: true });
});
}
}
function formatLegalValidityMeeting(VME, Meeting) {
return findCommissioners(VME).then((Commissioners) => {
const totals = Commissioners.reduce(
(acc, C) => {
C = C.toJSON();
let U = Meeting.Users.find((U) => C.id === U.id);
if (U && U.MeetingUser && U.MeetingUser.is_confirmed_at) {
acc.total_present_shares += C.representing_shares || 0;
acc.total_present += 1;
}
acc.total_shares += C.representing_shares || 0;
acc.total_commissioners += 1;
return acc;
},
{
total_present_shares: 0,
total_shares: 0,
total_present: 0,
total_commissioners: 0,
}
);
Meeting.setDataValue(
"is_legally_valid",
totals.total_present_shares / totals.total_shares > 0.5 &&
totals.total_present / totals.total_commissioners >= 0.5
);
Meeting.setDataValue("total_present_shares", totals.total_present_shares);
Meeting.setDataValue("total_shares", totals.total_shares);
Meeting.setDataValue("total_present", totals.total_present);
Meeting.setDataValue("total_commissioners", totals.total_commissioners);
return Meeting;
});
}
function formatMeetingVotes(Meeting) {
Meeting.MeetingItems = Meeting.MeetingItems.map((MeetingItem) => {
return setMeetingItemResults(MeetingItem);
});
return Meeting;
}
async function getMeetings(req, res) {
const { t } = req;
try {
// verify vme and get vme
let user = await userHelper.getAuthUser(req);
let VmeValidation = await ValidationHelper.validateVme(t, user.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
VME.getMeetings({
include: [
{
model: db.User,
attributes: {
exclude: [
"phone",
"email",
"email_verified_at",
"phone_verified_at",
],
},
},
{
model: db.MeetingItem,
include: [
{ model: db.FixedMeetingItem },
{
model: db.User,
as: "Users",
include: [
{
model: db.LotPhase,
include: [{ model: db.Lot, where: { VMEId: VME.id } }],
},
],
},
],
},
{ model: db.FinancialYear },
{ model: db.User, as: "Users" },
],
}).then(async (Meetings) => {
const Users = await VME.getUsers();
// update fixed meetings items
Meetings = Meetings.map((Meeting) => {
Meeting.MeetingItems = Meeting.MeetingItems.map((item) =>
formatMeetingItem(item)
);
Meeting = formatMeetingVotes(Meeting);
Meeting = formatPowerOfAttorneys(Meeting, Users);
return Meeting;
});
return res.json({
success: true,
Meetings: Meetings.sort(
(a, b) => new Date(b.starts_at) - new Date(a.starts_at)
),
});
});
} catch (error) {
return res.json({ success: false, error });
}
}
async function postMeeting(req, res) {
const { t } = req;
try {
let user = await userHelper.getAuthUser(req);
if (user) {
// verify vme and get vme
let VmeValidation = await ValidationHelper.validateVme(t, user.VMEId); //TODO: Validate user roles
if (!VmeValidation.success) {
return res.json(VmeValidation);
}
const { VME } = VmeValidation;
//TODO: update existing
const {
id,
name,
description,
is_cancelled,
starts_at,
hours,
type,
MeetingItems,
address,
FinancialYear,
} = req.body;
const data = {
id,
name,
description,
starts_at,
is_cancelled,
type,
address,
UserId: user.id,
VMEId: VME.id,
FinancialYearId: FinancialYear.id,
};
data.ends_at = new Date(starts_at);
data.ends_at.setTime(data.ends_at.getTime() + hours * 60 * 60 * 1000);
const ends_at = data.ends_at;
if (is_cancelled && id) {
// validation not required
} else {
var now = new Date();
let errors = {};
if (!name || name === "") {
errors["name"] = t(
"api.meetings.errors.nameRequired",
"Name is required."
);
}
if (!starts_at) {
// selected date is in the past
errors["starts_at"] = t(
"api.meetings.errors.startTimeRequired",
"Start time is required."
);
}
if (new Date(starts_at) < now) {
// selected date is in the past
errors["starts_at"] = t(
"api.meetings.errors.startTimeInPast",
"Start date is in the past."
);
}
if (new Date(ends_at) < now) {
// selected date is in the past
errors["ends_at"] = t(
"api.meetings.errors.endTimeInPast",
"End date is in the past."
);
}
if (new Date(ends_at) < starts_at) {
// selected date is in the past
errors["ends_at"] = t(
"api.meetings.errors.startTimeAfterEndTime",
"End date must be after the start date."
);
}
if (type === "general" && !address) {
// selected date is in the past
errors["address"] = t(
"api.meetings.errors.addressRequired",
"An address is required for a general meeting."
);
}
if (Object.keys(errors).length) {
return res.json({
success: false,
errors,
});
}
}
if (id) {
// update exisiting meeting
const Meeting = await db.Meeting.findOne({ where: { id } });
await Meeting.update(data);
jobReminderManager.relativeDatesAreChanged({
vmeId: VME.id,
type: "general_meeting",
});
await Promise.all(
MeetingItems.map(async (MeetingItem) => {
if (MeetingItem.id) {
await db.MeetingItem.update(MeetingItem, {
where: { id: MeetingItem.id },
});
} else {
await db.MeetingItem.create({
...MeetingItem,
MeetingId: Meeting.id,
});
}
})
);
// send mail to all users
// TODO: Use queue (cron-schedule / bull queue https://www.npmjs.com/package/bull)
const Users = await VME.getUsers();
Users.forEach((U) => {
notifier.notify(U, "update_meeting", {
Meeting,
VME,
});
});
return res.json({ success: true, Meeting });
} else {
await createMeeting(VME, data, MeetingItems, user);
return res.json({ success: true });
}
}
} catch (error) {
return res.json({ success: false, error });
}
}
async function createMeeting(VME, Meeting, MeetingItems, User) {
const createdMeeting = await db.Meeting.create(Meeting);
if (MeetingItems && MeetingItems.length) {
await db.MeetingItem.bulkCreate(
MeetingItems.map((item) => ({
...item,
MeetingId: createdMeeting.id,
UserId: User.id,
}))
);
}
jobReminderManager.relativeDatesAreChanged({
vmeId: VME.id,
type: "general_meeting",
});
// send mail to all users
// TODO: Use queue (cron-schedule / bull queue https://www.npmjs.com/package/bull)
const Users = await VME.getUsers();
// send notifications
Users.forEach((User) => {
notifier.notify(User, "new_meeting", {
Meeting: createdMeeting,
VME,
});
});
}
module.exports = router;