find matching real estates for new search request and notify
This commit is contained in:
@@ -25,6 +25,8 @@ const AWS_EMAIL_CONFIG = {
|
|||||||
|
|
||||||
const MAX_REAL_ESTATES_IN_EMAIL =
|
const MAX_REAL_ESTATES_IN_EMAIL =
|
||||||
parseInt(process.env.MAX_REAL_ESTATES_IN_EMAIL) || 10;
|
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 = {
|
module.exports = {
|
||||||
APP_PORT,
|
APP_PORT,
|
||||||
@@ -33,5 +35,6 @@ module.exports = {
|
|||||||
CRAWLER_INTERVAL,
|
CRAWLER_INTERVAL,
|
||||||
STOP_CRAWLER,
|
STOP_CRAWLER,
|
||||||
AWS_EMAIL_CONFIG,
|
AWS_EMAIL_CONFIG,
|
||||||
MAX_REAL_ESTATES_IN_EMAIL
|
MAX_REAL_ESTATES_IN_EMAIL,
|
||||||
|
MAX_REAL_ESTATES_IN_FIRST_EMAIL
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const db = require("../../models/index");
|
const db = require("../../models/index");
|
||||||
|
const sequelize = require("sequelize");
|
||||||
|
const Op = sequelize.Op;
|
||||||
|
|
||||||
const bulkUpsertRealEstates = async realEstateData => {
|
const bulkUpsertRealEstates = async realEstateData => {
|
||||||
try {
|
try {
|
||||||
@@ -26,10 +28,12 @@ const bulkUpsertRealEstates = async realEstateData => {
|
|||||||
"updatedAt",
|
"updatedAt",
|
||||||
"renewedDate"
|
"renewedDate"
|
||||||
];
|
];
|
||||||
|
const order = [["updatedAt", "desc"]];
|
||||||
|
|
||||||
return await db.RealEstate.bulkCreate(realEstateData, {
|
return await db.RealEstate.bulkCreate(realEstateData, {
|
||||||
updateOnDuplicate: fieldsToUpdateIfDuplicate,
|
updateOnDuplicate: fieldsToUpdateIfDuplicate,
|
||||||
returning: true
|
returning: true,
|
||||||
|
order
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error bulk upserting realEstates : ", e);
|
console.log("Error bulk upserting realEstates : ", e);
|
||||||
@@ -40,7 +44,68 @@ const getRealEstateById = async id => {
|
|||||||
return db.RealEstate.findByPk(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 = {
|
module.exports = {
|
||||||
bulkUpsertRealEstates,
|
bulkUpsertRealEstates,
|
||||||
getRealEstateById
|
getRealEstateById,
|
||||||
|
findRealEstatesForSearchRequest
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ const findRealEstatesForSearchRequest = async searchRequestId => {
|
|||||||
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({
|
const matches = await db.SearchRequestMatch.findAll({
|
||||||
where: query,
|
where: query,
|
||||||
include
|
include,
|
||||||
|
order
|
||||||
});
|
});
|
||||||
|
|
||||||
const matchingRealEstates = [];
|
const matchingRealEstates = [];
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ const generateEmailFooter = searchRequestId => {
|
|||||||
<strong>Vaš,<br/>Javimi tim</strong>`;
|
<strong>Vaš,<br/>Javimi tim</strong>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateRealEstateLinks = realEstates => {
|
||||||
|
let realEstateLinks = "";
|
||||||
|
for (const realEstate of realEstates) {
|
||||||
|
const { id: realEstateId, title } = realEstate;
|
||||||
|
|
||||||
|
realEstateLinks += `<li><a href="${APP_URL}/redirect/${realEstateId}">${title}</a></li>`;
|
||||||
|
}
|
||||||
|
return realEstateLinks;
|
||||||
|
};
|
||||||
|
|
||||||
const generateNotificationEmail = (realEstates, searchRequestId) => {
|
const generateNotificationEmail = (realEstates, searchRequestId) => {
|
||||||
const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL;
|
const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL;
|
||||||
const realEstatesToShow = truncateList
|
const realEstatesToShow = truncateList
|
||||||
@@ -17,16 +27,8 @@ const generateNotificationEmail = (realEstates, searchRequestId) => {
|
|||||||
: realEstates;
|
: realEstates;
|
||||||
|
|
||||||
const allRealEstatesLink = `${APP_URL}/nekretnine/${searchRequestId}`;
|
const allRealEstatesLink = `${APP_URL}/nekretnine/${searchRequestId}`;
|
||||||
|
const realEstateLinks = generateRealEstateLinks(realEstatesToShow);
|
||||||
let realEstateLinks = "";
|
|
||||||
for (const realEstate of realEstatesToShow) {
|
|
||||||
const { id: realEstateId, title } = realEstate;
|
|
||||||
|
|
||||||
realEstateLinks += `<li><a href="${APP_URL}/redirect/${realEstateId}">${title}</a></li><br />`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moreRealEstates = `<div>Kompletan spisak nekretnina možete pegledati na <a href="${allRealEstatesLink}">listi nekretnina</a><div>`;
|
const moreRealEstates = `<div>Kompletan spisak nekretnina možete pegledati na <a href="${allRealEstatesLink}">listi nekretnina</a><div>`;
|
||||||
|
|
||||||
const emailFooter = generateEmailFooter(searchRequestId);
|
const emailFooter = generateEmailFooter(searchRequestId);
|
||||||
|
|
||||||
return `<h3>Zdravo</h3>
|
return `<h3>Zdravo</h3>
|
||||||
@@ -40,7 +42,7 @@ const generateNotificationEmail = (realEstates, searchRequestId) => {
|
|||||||
${emailFooter}`;
|
${emailFooter}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateNewSearchRequestEmail = searchRequest => {
|
const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => {
|
||||||
const realEstateType = AD_CATEGORY[searchRequest.realEstateType];
|
const realEstateType = AD_CATEGORY[searchRequest.realEstateType];
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -52,8 +54,15 @@ const generateNewSearchRequestEmail = searchRequest => {
|
|||||||
priceMax
|
priceMax
|
||||||
} = searchRequest;
|
} = searchRequest;
|
||||||
|
|
||||||
|
const realEstateLinks = generateRealEstateLinks(matchingRealEstates);
|
||||||
|
const instantRealEstatesText = `<br/>
|
||||||
|
<div>
|
||||||
|
U međuvremenu pogledajte neke od nedavno objavljenih nekretnina koje odgovaraju Vašim uslovima pretrage :<br/>
|
||||||
|
${realEstateLinks}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
const gardenSize = realEstateType.hasGardenSize
|
const gardenSize = realEstateType.hasGardenSize
|
||||||
? `<div><strong>Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2 </strong></div>`
|
? `<div><strong>Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2</strong></div>`
|
||||||
: ``;
|
: ``;
|
||||||
|
|
||||||
const emailFooter = generateEmailFooter(id);
|
const emailFooter = generateEmailFooter(id);
|
||||||
@@ -68,6 +77,7 @@ const generateNewSearchRequestEmail = searchRequest => {
|
|||||||
${gardenSize}
|
${gardenSize}
|
||||||
<div><strong>Cijena:</strong> ${priceMin} do ${priceMax} KM</div>
|
<div><strong>Cijena:</strong> ${priceMin} do ${priceMax} KM</div>
|
||||||
</div>
|
</div>
|
||||||
|
${matchingRealEstates.length > 0 ? instantRealEstatesText : ""}
|
||||||
<br/>
|
<br/>
|
||||||
${emailFooter}`;
|
${emailFooter}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const { matchRealEstates } = require("../services/searchMatchService");
|
const {
|
||||||
|
matchRealEstates,
|
||||||
|
matchSearchRequest
|
||||||
|
} = require("../services/searchMatchService");
|
||||||
const {
|
const {
|
||||||
generateNotificationEmail,
|
generateNotificationEmail,
|
||||||
generateNewSearchRequestEmail
|
generateNewSearchRequestEmail
|
||||||
@@ -8,6 +11,24 @@ const { sendEmail } = require("../services/emailService");
|
|||||||
|
|
||||||
const notifyForNewRealEstates = async newRealEstates => {
|
const notifyForNewRealEstates = async newRealEstates => {
|
||||||
const matches = await matchRealEstates(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 searchRequestsToNotify = Object.keys(matches);
|
||||||
|
|
||||||
const asyncSendEmailActions = [];
|
const asyncSendEmailActions = [];
|
||||||
@@ -27,12 +48,6 @@ const notifyForNewRealEstates = async newRealEstates => {
|
|||||||
await Promise.all(asyncSendEmailActions);
|
await Promise.all(asyncSendEmailActions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const notifyForNewSearchRequest = async searchRequest => {
|
|
||||||
const emailContent = generateNewSearchRequestEmail(searchRequest);
|
|
||||||
const { email } = searchRequest;
|
|
||||||
await sendEmail(email, "Market Alert", emailContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
notifyForNewRealEstates,
|
notifyForNewRealEstates,
|
||||||
notifyForNewSearchRequest
|
notifyForNewSearchRequest
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
const {
|
const {
|
||||||
findSearchRequestsForRealEstate
|
findSearchRequestsForRealEstate
|
||||||
} = require("../helpers/db/searchRequest");
|
} = require("../helpers/db/searchRequest");
|
||||||
|
const { findRealEstatesForSearchRequest } = require("../helpers/db/realEstate");
|
||||||
const { addMatches } = require("../helpers/db/searchRequestMatch");
|
const { addMatches } = require("../helpers/db/searchRequestMatch");
|
||||||
|
const { MAX_REAL_ESTATES_IN_FIRST_EMAIL } = require("../config/appConfig");
|
||||||
|
|
||||||
const matchRealEstates = async realEstates => {
|
const matchRealEstates = async realEstates => {
|
||||||
if (Array.isArray(realEstates)) {
|
if (Array.isArray(realEstates)) {
|
||||||
@@ -40,6 +42,35 @@ const matchRealEstates = async realEstates => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
const matchSearchRequest = async searchRequest => {
|
||||||
matchRealEstates
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ SEQUELIZE_LOGGING=0- no sequelize logging, 1- log to the console
|
|||||||
PORT=Port for the app, defaults to 5000
|
PORT=Port for the app, defaults to 5000
|
||||||
APP_BASE_URL=base url for the app
|
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_KEY_ID=(your-key-here)
|
||||||
AWS_SECRET_ACCESS_KEY=(your-key-here)
|
AWS_SECRET_ACCESS_KEY=(your-key-here)
|
||||||
|
|||||||
Reference in New Issue
Block a user