From 615aed65b6f4075fe65a2ea1695fd7d7d33ca6c8 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Mon, 7 Oct 2019 20:49:54 +0200 Subject: [PATCH] find matching real estates for new search request and notify --- app/config/appConfig.js | 5 +- app/helpers/db/realEstate.js | 69 +++++++++++++++++++++++++++- app/helpers/db/searchRequestMatch.js | 7 ++- app/helpers/emailContentGenerator.js | 32 ++++++++----- app/services/notificationService.js | 29 +++++++++--- app/services/searchMatchService.js | 35 +++++++++++++- development.env | 3 +- 7 files changed, 154 insertions(+), 26 deletions(-) diff --git a/app/config/appConfig.js b/app/config/appConfig.js index c56f93b..8a11650 100644 --- a/app/config/appConfig.js +++ b/app/config/appConfig.js @@ -25,6 +25,8 @@ const AWS_EMAIL_CONFIG = { const MAX_REAL_ESTATES_IN_EMAIL = parseInt(process.env.MAX_REAL_ESTATES_IN_EMAIL) || 10; +const MAX_REAL_ESTATES_IN_FIRST_EMAIL = + parseInt(process.env.MAX_REAL_ESTATES_IN_FIRST_EMAIL) || 5; module.exports = { APP_PORT, @@ -33,5 +35,6 @@ module.exports = { CRAWLER_INTERVAL, STOP_CRAWLER, AWS_EMAIL_CONFIG, - MAX_REAL_ESTATES_IN_EMAIL + MAX_REAL_ESTATES_IN_EMAIL, + MAX_REAL_ESTATES_IN_FIRST_EMAIL }; diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index fc5669e..fd9f086 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -1,5 +1,7 @@ "use strict"; const db = require("../../models/index"); +const sequelize = require("sequelize"); +const Op = sequelize.Op; const bulkUpsertRealEstates = async realEstateData => { try { @@ -26,10 +28,12 @@ const bulkUpsertRealEstates = async realEstateData => { "updatedAt", "renewedDate" ]; + const order = [["updatedAt", "desc"]]; return await db.RealEstate.bulkCreate(realEstateData, { updateOnDuplicate: fieldsToUpdateIfDuplicate, - returning: true + returning: true, + order }); } catch (e) { console.log("Error bulk upserting realEstates : ", e); @@ -40,7 +44,68 @@ const getRealEstateById = async id => { return db.RealEstate.findByPk(id); }; +const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { + const { + priceMin, + priceMax, + sizeMin, + sizeMax, + adType, + realEstateType, + areaToSearch + } = searchRequest; + + const longitudeColumn = sequelize.col("locationLong"); + const latitudeColumn = sequelize.col("locationLat"); + + const pointGeometry = sequelize.fn( + "ST_Point", + longitudeColumn, + latitudeColumn + ); + const pointWithSRID = sequelize.fn("ST_SetSRID", pointGeometry, 4326); + const areaToSearchAsGeometry = sequelize.fn( + "ST_GeomFromGeoJSON", + JSON.stringify(areaToSearch) + ); + const areaToSearchWithSRID = sequelize.fn( + "ST_SetSRID", + areaToSearchAsGeometry, + 4326 + ); + const contains = sequelize.fn( + "ST_Contains", + areaToSearchWithSRID, + pointWithSRID + ); + + const geoSearchQueryPart = sequelize.where(contains, true); + + const query = { + adType, + realEstateType, + price: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + }, + area: { + [Op.lte]: sizeMax, + [Op.gte]: sizeMin + }, + [Op.and]: geoSearchQueryPart + }; + + const order = [["updatedAt", "desc"]]; + + return await db.RealEstate.findAll({ + where: query, + limit: maxResults, + order + }); +}; + module.exports = { bulkUpsertRealEstates, - getRealEstateById + getRealEstateById, + findRealEstatesForSearchRequest }; diff --git a/app/helpers/db/searchRequestMatch.js b/app/helpers/db/searchRequestMatch.js index cb9e9c3..d7cb215 100644 --- a/app/helpers/db/searchRequestMatch.js +++ b/app/helpers/db/searchRequestMatch.js @@ -6,11 +6,14 @@ const findRealEstatesForSearchRequest = async searchRequestId => { searchRequestId }; - const include = [{ model: db.RealEstate, as: "realEstates" }]; + const realEstatesModel = { model: db.RealEstate, as: "realEstates" }; + const order = [[realEstatesModel, "updatedAt", "desc"]]; + const include = [realEstatesModel]; const matches = await db.SearchRequestMatch.findAll({ where: query, - include + include, + order }); const matchingRealEstates = []; diff --git a/app/helpers/emailContentGenerator.js b/app/helpers/emailContentGenerator.js index ec4d35c..2236d02 100644 --- a/app/helpers/emailContentGenerator.js +++ b/app/helpers/emailContentGenerator.js @@ -10,6 +10,16 @@ const generateEmailFooter = searchRequestId => { Vaš,
Javimi tim
`; }; +const generateRealEstateLinks = realEstates => { + let realEstateLinks = ""; + for (const realEstate of realEstates) { + const { id: realEstateId, title } = realEstate; + + realEstateLinks += `
  • ${title}
  • `; + } + return realEstateLinks; +}; + const generateNotificationEmail = (realEstates, searchRequestId) => { const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL; const realEstatesToShow = truncateList @@ -17,16 +27,8 @@ const generateNotificationEmail = (realEstates, searchRequestId) => { : realEstates; const allRealEstatesLink = `${APP_URL}/nekretnine/${searchRequestId}`; - - let realEstateLinks = ""; - for (const realEstate of realEstatesToShow) { - const { id: realEstateId, title } = realEstate; - - realEstateLinks += `
  • ${title}

  • `; - } - + const realEstateLinks = generateRealEstateLinks(realEstatesToShow); const moreRealEstates = `
    Kompletan spisak nekretnina možete pegledati na listi nekretnina
    `; - const emailFooter = generateEmailFooter(searchRequestId); return `

    Zdravo

    @@ -40,7 +42,7 @@ const generateNotificationEmail = (realEstates, searchRequestId) => { ${emailFooter}`; }; -const generateNewSearchRequestEmail = searchRequest => { +const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => { const realEstateType = AD_CATEGORY[searchRequest.realEstateType]; const { id, @@ -52,8 +54,15 @@ const generateNewSearchRequestEmail = searchRequest => { priceMax } = searchRequest; + const realEstateLinks = generateRealEstateLinks(matchingRealEstates); + const instantRealEstatesText = `
    +
    + U međuvremenu pogledajte neke od nedavno objavljenih nekretnina koje odgovaraju Vašim uslovima pretrage :
    + ${realEstateLinks} +
    `; + const gardenSize = realEstateType.hasGardenSize - ? `
    Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2
    ` + ? `
    Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2
    ` : ``; const emailFooter = generateEmailFooter(id); @@ -68,6 +77,7 @@ const generateNewSearchRequestEmail = searchRequest => { ${gardenSize}
    Cijena: ${priceMin} do ${priceMax} KM
    + ${matchingRealEstates.length > 0 ? instantRealEstatesText : ""}
    ${emailFooter}`; }; diff --git a/app/services/notificationService.js b/app/services/notificationService.js index 52d171a..b0c84ff 100644 --- a/app/services/notificationService.js +++ b/app/services/notificationService.js @@ -1,5 +1,8 @@ "use strict"; -const { matchRealEstates } = require("../services/searchMatchService"); +const { + matchRealEstates, + matchSearchRequest +} = require("../services/searchMatchService"); const { generateNotificationEmail, generateNewSearchRequestEmail @@ -8,6 +11,24 @@ const { sendEmail } = require("../services/emailService"); const notifyForNewRealEstates = async newRealEstates => { const matches = await matchRealEstates(newRealEstates); + await notifyMatches(matches); +}; + +const notifyForNewSearchRequest = async searchRequest => { + const matches = await matchSearchRequest(searchRequest); + + const searchRequestId = searchRequest.id; + const matchingRealEstates = matches[searchRequestId].realEstates; + + const emailContent = generateNewSearchRequestEmail( + searchRequest, + matchingRealEstates + ); + const { email } = searchRequest; + await sendEmail(email, "Market Alert", emailContent); +}; + +const notifyMatches = async matches => { const searchRequestsToNotify = Object.keys(matches); const asyncSendEmailActions = []; @@ -27,12 +48,6 @@ const notifyForNewRealEstates = async newRealEstates => { await Promise.all(asyncSendEmailActions); }; -const notifyForNewSearchRequest = async searchRequest => { - const emailContent = generateNewSearchRequestEmail(searchRequest); - const { email } = searchRequest; - await sendEmail(email, "Market Alert", emailContent); -}; - module.exports = { notifyForNewRealEstates, notifyForNewSearchRequest diff --git a/app/services/searchMatchService.js b/app/services/searchMatchService.js index 1075bee..4c83504 100644 --- a/app/services/searchMatchService.js +++ b/app/services/searchMatchService.js @@ -3,7 +3,9 @@ const { findSearchRequestsForRealEstate } = require("../helpers/db/searchRequest"); +const { findRealEstatesForSearchRequest } = require("../helpers/db/realEstate"); const { addMatches } = require("../helpers/db/searchRequestMatch"); +const { MAX_REAL_ESTATES_IN_FIRST_EMAIL } = require("../config/appConfig"); const matchRealEstates = async realEstates => { if (Array.isArray(realEstates)) { @@ -40,6 +42,35 @@ const matchRealEstates = async realEstates => { } }; -module.exports = { - matchRealEstates +const matchSearchRequest = async searchRequest => { + const { id: searchRequestId } = searchRequest; + + const realEstates = await findRealEstatesForSearchRequest( + searchRequest, + MAX_REAL_ESTATES_IN_FIRST_EMAIL + ); + const matches = { + [searchRequestId]: { + searchRequest, + realEstates: [] + } + }; + const matchingRecords = []; + + for (const realEstate of realEstates) { + matches[searchRequestId].realEstates.push(realEstate); + matchingRecords.push({ + searchRequestId, + realEstateId: realEstate.id, + notified: false + }); + } + + await addMatches(matchingRecords); + return matches; +}; + +module.exports = { + matchRealEstates, + matchSearchRequest }; diff --git a/development.env b/development.env index 6368ee7..94d8549 100644 --- a/development.env +++ b/development.env @@ -8,7 +8,8 @@ SEQUELIZE_LOGGING=0- no sequelize logging, 1- log to the console PORT=Port for the app, defaults to 5000 APP_BASE_URL=base url for the app -MAX_REAL_ESTATES_IN_EMAIL=Number of real estates that will be shown in URL, others will be truncated and URL with full list will be shwon +MAX_REAL_ESTATES_IN_EMAIL=Max number of real estates that will be shown in email, others will be truncated and URL with full list will be shwon +MAX_REAL_ESTATES_IN_FIRST_EMAIL=Max number of real estates that will be shown in first (welcome) email AWS_KEY_ID=(your-key-here) AWS_SECRET_ACCESS_KEY=(your-key-here)