diff --git a/app/common/enums.js b/app/common/enums.js index efdbe28..942fb47 100644 --- a/app/common/enums.js +++ b/app/common/enums.js @@ -174,10 +174,24 @@ const CRAWLER_AD_TYPE = { ONLY_REQUEST: 4 }; +const EMAIL_FREQUENCY = { + ASAP: { + id: 1, + stringId: "ASAP", + title: "Odmah" + }, + DAILY: { + id: 2, + stringId: "DAILY", + title: "Jednom dnevno" + } +}; + module.exports = { AD_TYPE, AD_CATEGORY, AD_STATUS, AD_AGENCY, - CRAWLER_AD_TYPE + CRAWLER_AD_TYPE, + EMAIL_FREQUENCY }; diff --git a/app/controllers/queryReview.js b/app/controllers/queryReview.js index 2b08d82..da2f42a 100644 --- a/app/controllers/queryReview.js +++ b/app/controllers/queryReview.js @@ -3,9 +3,9 @@ const { isValidEmail } = require("../helpers/email"); const { notifyForNewSearchRequest } = require("../services/notificationService"); -const { AD_CATEGORY, AD_TYPE } = require("../common/enums"); +const { AD_CATEGORY, AD_TYPE, EMAIL_FREQUENCY } = require("../common/enums"); -const getQueryReviewData = searchRequest => { +const getQueryReviewTableData = searchRequest => { const { id, adType, @@ -87,15 +87,26 @@ const getQueryReview = async (req, res) => { const title = "Da li je ovo to što ste tražili ?"; const nextStep = req.query.nextStep; const error = req.query.error; - const queryReviewData = getQueryReviewData(searchRequest); + const queryReviewTableData = getQueryReviewTableData(searchRequest); const email = searchRequest.email; + let selectedEmailFrequency; + switch (searchRequest.emailFrequency) { + case EMAIL_FREQUENCY.ASAP.stringId: + selectedEmailFrequency = EMAIL_FREQUENCY.ASAP.id; + break; + case EMAIL_FREQUENCY.DAILY.stringId: + selectedEmailFrequency = EMAIL_FREQUENCY.DAILY.id; + break; + } res.render("queryReview", { nextStep, - queryReviewData, + queryReviewTableData, title, email, - error + selectedEmailFrequency, + error, + EMAIL_FREQUENCY }); }; @@ -107,17 +118,26 @@ const postQueryReview = async (req, res) => { } const nextStep = req.query.nextStep || "/ponovo"; + const emailFrequency = + parseInt(req.body.emailFrequency) || EMAIL_FREQUENCY.ASAP.id; const emailInput = req.body.email; const emailConfirmInput = req.body.confirmEmail; const title = "Da li je ovo to što ste tražili ?"; - const queryReviewData = getQueryReviewData(searchRequest); + const queryReviewTableData = getQueryReviewTableData(searchRequest); + + let emailFrequencyStringId = EMAIL_FREQUENCY.ASAP.stringId; + if (emailFrequency === EMAIL_FREQUENCY.DAILY.id) { + emailFrequencyStringId = EMAIL_FREQUENCY.DAILY.stringId; + } + + searchRequest.emailFrequency = emailFrequencyStringId; if (emailInput !== emailConfirmInput) { const error = "Greška ! Unešeni emailovi nisu isti"; res.render("queryReview", { error, title, - queryReviewData, + queryReviewTableData, email: "" }); return; @@ -128,7 +148,7 @@ const postQueryReview = async (req, res) => { res.render("queryReview", { error, title, - queryReviewData, + queryReviewTableData, email: "" }); return; @@ -147,7 +167,7 @@ const postQueryReview = async (req, res) => { res.render("queryReview", { error, title, - queryReviewData, + queryReviewTableData, email: "" }); return; @@ -164,7 +184,7 @@ const postQueryReview = async (req, res) => { res.render("queryReview", { error, title, - queryReviewData, + queryReviewTableData, email: "" }); return; diff --git a/app/helpers/db/searchRequestMatch.js b/app/helpers/db/searchRequestMatch.js index d7cb215..11d5cde 100644 --- a/app/helpers/db/searchRequestMatch.js +++ b/app/helpers/db/searchRequestMatch.js @@ -24,6 +24,23 @@ const findRealEstatesForSearchRequest = async searchRequestId => { return matchingRealEstates; }; +const findNotNotifiedMatches = async () => { + const query = { + notified: false + }; + + const searchRequestsModel = { model: db.SearchRequest, as: "searchRequests" }; + const realEstateModel = { model: db.RealEstate, as: "realEstates" }; + const include = [searchRequestsModel, realEstateModel]; + + const matchingRecords = await db.SearchRequestMatch.findAll({ + where: query, + include + }); + + return matchingRecords; +}; + const addMatches = async matchingRecords => { return await db.SearchRequestMatch.bulkCreate(matchingRecords, { ignoreDuplicates: true @@ -32,5 +49,6 @@ const addMatches = async matchingRecords => { module.exports = { findRealEstatesForSearchRequest, - addMatches + addMatches, + findNotNotifiedMatches }; diff --git a/app/helpers/emailContentGenerator.js b/app/helpers/emailContentGenerator.js index 549883e..861bfb2 100644 --- a/app/helpers/emailContentGenerator.js +++ b/app/helpers/emailContentGenerator.js @@ -20,7 +20,11 @@ const generateRealEstateLinks = realEstates => { return realEstateLinks; }; -const generateNotificationEmail = (realEstates, searchRequestId) => { +const generateNotificationEmail = ( + realEstates, + searchRequestId, + dailyNotification = false +) => { const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL; const realEstatesToShow = truncateList ? realEstates.slice(0, MAX_REAL_ESTATES_IN_EMAIL) @@ -30,9 +34,20 @@ const generateNotificationEmail = (realEstates, searchRequestId) => { const realEstateLinks = generateRealEstateLinks(realEstatesToShow); const moreRealEstates = `
Kompletan spisak nekretnina možete pogledati na listi nekretnina
`; const emailFooter = generateEmailFooter(searchRequestId); + const asapMessageBody = + realEstates.length > 1 + ? "Pronašli smo nekretnine koje odgovaraju Vašoj pretrazi" + : "Pronašli smo nekretninu koja odgovara Vašoj pretrazi"; + + const dailyMessageBody = + realEstates.length > 1 + ? "U posljednja 24h objavljene su sljedeće nekretnine koje odgovaraju uslovima Vaše pretrage" + : "U posljednja 24h objavljena je sljedeća nekretnina koja odgovara uslovima Vaše pretrage"; + + const messageBody = dailyNotification ? dailyMessageBody : asapMessageBody; return `

Zdravo

-

Pronašli smo nekretnine koje odgovaraju Vašoj pretrazi

+

${messageBody}

${realEstateLinks}
diff --git a/app/migrations/20191101113541-add-emailFrequency-column-to-searchRequest-table.js b/app/migrations/20191101113541-add-emailFrequency-column-to-searchRequest-table.js new file mode 100644 index 0000000..3d39291 --- /dev/null +++ b/app/migrations/20191101113541-add-emailFrequency-column-to-searchRequest-table.js @@ -0,0 +1,15 @@ +"use strict"; +const { EMAIL_FREQUENCY } = require("../common/enums"); + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.addColumn("SearchRequests", "emailFrequency", { + type: Sequelize.TEXT, + defaultValue: EMAIL_FREQUENCY.ASAP.stringId + }); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.removeColumn("SearchRequests", "emailFrequency"); + } +}; diff --git a/app/models/searchRequest.js b/app/models/searchRequest.js index 71ab26e..68483ed 100644 --- a/app/models/searchRequest.js +++ b/app/models/searchRequest.js @@ -1,6 +1,6 @@ "use strict"; -const { AD_TYPE } = require("../common/enums"); +const { AD_TYPE, EMAIL_FREQUENCY } = require("../common/enums"); module.exports = (sequelize, DataTypes) => { const SearchRequest = sequelize.define("SearchRequest", { @@ -61,6 +61,11 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false + }, + emailFrequency: { + type: DataTypes.TEXT, + defaultValue: EMAIL_FREQUENCY.ASAP.stringId, + allowNull: false } }); diff --git a/app/models/searchRequestMatch.js b/app/models/searchRequestMatch.js index 19f367d..0e2e392 100644 --- a/app/models/searchRequestMatch.js +++ b/app/models/searchRequestMatch.js @@ -44,6 +44,12 @@ module.exports = (sequelize, DataTypes) => { ); SearchRequestMatch.associate = models => { + SearchRequestMatch.hasMany(models.SearchRequest, { + foreignKey: "id", + sourceKey: "searchRequestId", + targetKey: "id", + as: "searchRequests" + }); SearchRequestMatch.hasMany(models.RealEstate, { foreignKey: "id", as: "realEstates" diff --git a/app/npmScripts/npmDailyNotify.js b/app/npmScripts/npmDailyNotify.js new file mode 100644 index 0000000..8eb6d33 --- /dev/null +++ b/app/npmScripts/npmDailyNotify.js @@ -0,0 +1,8 @@ +"use strict"; +const { + notifyRequestsWithDailyOption +} = require("../services/notificationService"); + +(async () => { + await notifyRequestsWithDailyOption(); +})(); diff --git a/app/public/segment.css b/app/public/segment.css index fdd0372..313de5d 100644 --- a/app/public/segment.css +++ b/app/public/segment.css @@ -1,14 +1,18 @@ -.ui-segment { +.segmented { color: #02adba; border: 1px solid #02adba; border-radius: 4px; display: inline-block; } -.ui-segment span.option.active { +.segmented label { + color: #02adba; +} +.segmented input:checked + .label { background-color: #02adba; color: white; } -.ui-segment span.option { +[type="radio"]:not(:checked) + span, +[type="radio"]:checked + span { padding-left: 30px; padding-right: 30px; height: 35px; @@ -21,9 +25,14 @@ border-right: 1px solid #02adba; } -.ui-segment span.option:last-child { +.segmented :last-child .label { border-right: none; } -.segment-select { +.segmented input { + display: none; +} + +span.label:before, +span.label:after { display: none; } diff --git a/app/services/notificationService.js b/app/services/notificationService.js index 7a6c2d5..194d3f6 100644 --- a/app/services/notificationService.js +++ b/app/services/notificationService.js @@ -8,6 +8,7 @@ const { generateNewSearchRequestEmail, generateEmailSubject } = require("../helpers/emailContentGenerator"); +const { findNotNotifiedMatches } = require("../helpers/db/searchRequestMatch"); const { sendEmail } = require("../services/emailService"); const notifyForNewRealEstates = async newRealEstates => { @@ -29,34 +30,87 @@ const notifyForNewSearchRequest = async searchRequest => { await sendEmail(email, "Kivi - novi zahtjev za pretragu", emailContent); }; -const notifyMatches = async matches => { +const notifyMatches = async (matches, dailyNotification = false) => { const searchRequestsToNotify = Object.keys(matches); const asyncSendEmailActions = []; for (const id of searchRequestsToNotify) { - const { searchRequest } = matches[id]; - const { email } = searchRequest; - const allMatchingRealEstates = matches[id].realEstates || []; - if (allMatchingRealEstates.length > 0) { - const emailContent = generateNotificationEmail( - allMatchingRealEstates, - id - ); - const emailSubject = generateEmailSubject( - allMatchingRealEstates.length, - allMatchingRealEstates[0].title - ); + const { searchRequest, notifyNow } = matches[id]; + if (notifyNow) { + const { email } = searchRequest; + const allMatchingRealEstates = matches[id].realEstates || []; + if (allMatchingRealEstates.length > 0) { + const emailContent = generateNotificationEmail( + allMatchingRealEstates, + id, + dailyNotification + ); + const emailSubject = generateEmailSubject( + allMatchingRealEstates.length, + allMatchingRealEstates[0].title + ); - const sendEmailPromise = sendEmail(email, emailSubject, emailContent); - asyncSendEmailActions.push(sendEmailPromise); - sendEmailPromise.catch(err => console.log("[Email Sending Failed]", err)); + const sendEmailPromise = sendEmail(email, emailSubject, emailContent); + asyncSendEmailActions.push(sendEmailPromise); + sendEmailPromise.catch(err => + console.log("[Email Sending Failed]", err) + ); + } } } await Promise.all(asyncSendEmailActions); }; +const notifyRequestsWithDailyOption = async () => { + const notNotifiedSearchRequestMatches = await findNotNotifiedMatches(); + + const matches = {}; + + for (const searchRequestMatch of notNotifiedSearchRequestMatches) { + const { searchRequests, realEstates } = searchRequestMatch; + + if (!Array.isArray(searchRequests) || searchRequests.length !== 1) { + // Something is wrong with this match + // (search request not found for specified search request id) + // OR + // there are multiple search requests with the same ID (this should never be the case ! + // TODO: Maybe if association is defined better, this will be automatically only one object instead of array + continue; + } + + if (!Array.isArray(realEstates) || realEstates.length !== 1) { + // Something is wrong with this match + // (real estate not found for specified real estate id) + // OR + // there are multiple real estates with the same ID (this should never be the case ! + // TODO: Maybe if association is defined better, this will be automatically only one object instead of array + continue; + } + + const searchRequest = searchRequests[0]; + const realEstate = realEstates[0]; + const searchRequestId = searchRequest.id; + + if (!matches[searchRequestId]) { + matches[searchRequestId] = { + searchRequest, + realEstates: [], + notifyNow: true + }; + } + + matches[searchRequestId].realEstates.push(realEstate); + + searchRequestMatch.notified = true; + searchRequestMatch.save(); + } + + await notifyMatches(matches, true); +}; + module.exports = { notifyForNewRealEstates, - notifyForNewSearchRequest + notifyForNewSearchRequest, + notifyRequestsWithDailyOption }; diff --git a/app/services/searchMatchService.js b/app/services/searchMatchService.js index 4c83504..f39d21d 100644 --- a/app/services/searchMatchService.js +++ b/app/services/searchMatchService.js @@ -6,6 +6,7 @@ const { const { findRealEstatesForSearchRequest } = require("../helpers/db/realEstate"); const { addMatches } = require("../helpers/db/searchRequestMatch"); const { MAX_REAL_ESTATES_IN_FIRST_EMAIL } = require("../config/appConfig"); +const { EMAIL_FREQUENCY } = require("../common/enums"); const matchRealEstates = async realEstates => { if (Array.isArray(realEstates)) { @@ -18,18 +19,19 @@ const matchRealEstates = async realEstates => { searchRequestsPromise.then(searchRequests => { for (const searchRequest of searchRequests) { - const { id } = searchRequest; + const { id, emailFrequency } = searchRequest; if (!matches[id]) { matches[id] = { searchRequest, - realEstates: [] + realEstates: [], + notifyNow: emailFrequency === EMAIL_FREQUENCY.ASAP.stringId }; } matches[id].realEstates.push(realEstate); matchingRecords.push({ searchRequestId: searchRequest.id, realEstateId: realEstate.id, - notified: false + notified: emailFrequency === EMAIL_FREQUENCY.ASAP.stringId }); } }); @@ -62,7 +64,7 @@ const matchSearchRequest = async searchRequest => { matchingRecords.push({ searchRequestId, realEstateId: realEstate.id, - notified: false + notified: true }); } diff --git a/app/views/queryReview.ejs b/app/views/queryReview.ejs index 115b067..7e0f6e4 100644 --- a/app/views/queryReview.ejs +++ b/app/views/queryReview.ejs @@ -2,7 +2,7 @@
    - <% for(const stepData of queryReviewData) { %> + <% for(const stepData of queryReviewTableData) { %>
  • <%= stepData.title || '-' %> @@ -13,6 +13,26 @@ <% } %>
+
+
Slanje obavještenja
+ + + + + +
value="<%= email %>" <% } %> required size="250" /> @@ -47,6 +67,7 @@