Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into karen/unit-list-view
Browse files Browse the repository at this point in the history
  • Loading branch information
petabite committed Jun 4, 2024
2 parents d48dc20 + 2d3228d commit a6a1c9a
Show file tree
Hide file tree
Showing 35 changed files with 1,595 additions and 283 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
echo MONGODB_URI=${{ secrets.MONGODB_URI }} >> .env
echo FRONTEND_ORIGIN=${{ vars.FRONTEND_ORIGIN }} >> .env
echo FIREBASE=${{ secrets.FIREBASE }} >> .env
echo GMAILUSER=${{ secrets.GMAILUSER }} >> .env
echo GMAILPASS=${{ secrets.GMAILPASS }} >> .env
- name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0
with:
Expand Down
Binary file modified backend/bun.lockb
Binary file not shown.
6 changes: 4 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
"dotenv": "^16.3.1",
"envalid": "^7.3.1",
"express": "^4.18.2",
"express-validator": "^7.0.1",
"express-validator": "^7.1.0",
"firebase-admin": "^12.0.0",
"firebase-functions": "^4.7.0",
"http-errors": "^2.0.0",
"module-alias": "^2.2.3",
"mongodb": "^5.7.0",
"mongoose": "^7.4.0"
"mongoose": "^7.4.0",
"nodemailer": "^6.9.13"
},
"name": "backend",
"version": "1.0.0",
Expand All @@ -32,6 +33,7 @@
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/http-errors": "^2.0.1",
"@types/nodemailer": "^6.4.15",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"bun-types": "latest",
Expand Down
12 changes: 11 additions & 1 deletion backend/src/controllers/referral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ObjectId } from "mongoose";

import { asyncHandler } from "./wrappers";

import { createReferral, editReferral } from "@/services/referral";
import { createReferral, deleteReferral, editReferral } from "@/services/referral";

type CreateReferralRequestBody = {
renterCandidateId: string;
Expand Down Expand Up @@ -43,3 +43,13 @@ export const editReferralHandler: RequestHandler = asyncHandler(async (req, res,
res.status(400);
}
});

export const deleteReferralHandler: RequestHandler = asyncHandler(async (req, res, _) => {
const id = req.params.id;
const response = await deleteReferral(id);
if (response === null) {
res.status(400);
} else {
res.status(200).json(response);
}
});
22 changes: 21 additions & 1 deletion backend/src/controllers/renter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import createHttpError from "http-errors";

import { asyncHandler } from "./wrappers";

import { createRenterCandidate, getRenterCandidate, getRenterCandidates } from "@/services/renter";
import {
createRenterCandidate,
editRenterCandidate,
getRenterCandidate,
getRenterCandidates,
} from "@/services/renter";

export const getRenterCandidatesHandler: RequestHandler = asyncHandler(async (_req, res, _next) => {
const renters = await getRenterCandidates();
Expand All @@ -22,6 +27,8 @@ type CreateRenterCandidateRequestBody = {
email?: string;
};

type EditRenterCandidateRequestBody = Partial<CreateRenterCandidateRequestBody>;

export const createRenterCandidateHandler: RequestHandler = asyncHandler(async (req, res, _) => {
const { firstName, lastName, uid, program, adults, children, phone, email } =
req.body as CreateRenterCandidateRequestBody;
Expand Down Expand Up @@ -56,3 +63,16 @@ export const getRenterCandidateHandler: RequestHandler = asyncHandler(async (req

res.status(200).json(body);
});

export const editRenterCandidateHandler: RequestHandler = asyncHandler(async (req, res) => {
const { id } = req.params;
const editQuery = req.body as EditRenterCandidateRequestBody;

const result = await editRenterCandidate(id, editQuery);

if (result === null) {
res.status(404);
} else {
res.status(200).json(result);
}
});
2 changes: 1 addition & 1 deletion backend/src/middleware/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { RequestHandler } from "express";
import { ContextRunner, ValidationChain, validationResult } from "express-validator";
import { Middleware } from "express-validator/src/base";
import { Middleware } from "express-validator/lib/base";
import createHttpError from "http-errors";

/**
Expand Down
1 change: 1 addition & 0 deletions backend/src/models/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const unitSchema = new Schema(
numBeds: { type: Number, required: true },
numBaths: { type: Number, required: true },
appliances: { type: [String], required: true },
utilities: { type: [String], required: true },
communityFeatures: { type: [String], required: true },
parking: { type: [String], required: true },
accessibility: { type: [String], required: true },
Expand Down
8 changes: 7 additions & 1 deletion backend/src/routes/referral.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import express from "express";

import { createReferralHandler, editReferralHandler } from "@/controllers/referral";
import {
createReferralHandler,
deleteReferralHandler,
editReferralHandler,
} from "@/controllers/referral";
import { requireUser } from "@/middleware/auth";

const router = express.Router();
Expand All @@ -9,4 +13,6 @@ router.post("/", requireUser, createReferralHandler);

router.put("/:id", requireUser, editReferralHandler);

router.delete("/:id", requireUser, deleteReferralHandler);

export default router;
2 changes: 2 additions & 0 deletions backend/src/routes/renter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ router.post(
RenterController.createRenterCandidateHandler,
);

router.put("/:id", requireUser, RenterController.editRenterCandidateHandler);

export default router;
26 changes: 26 additions & 0 deletions backend/src/services/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import "dotenv/config";

import { createTransport } from "nodemailer";

export async function sendEmail(recipient: string, subjectString: string, body: string) {
const transporter = createTransport({
port: 465, // true for 465, false for other ports
host: "smtp.gmail.com",
auth: {
user: process.env.GMAILUSER,
pass: process.env.GMAILPASS,
},
secure: true,
});

const mailData = {
from: process.env.GMAILUSER, // sender address
to: recipient, // list of receivers
subject: subjectString,
text: body,
};

const response = await transporter.sendMail(mailData);

return response;
}
47 changes: 47 additions & 0 deletions backend/src/services/referral.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ObjectId } from "mongoose";

import { sendEmail } from "./email";
import { getUserByID } from "./user";

import { ReferralModel } from "@/models/referral";

export async function getUnitReferrals(id: string) {
Expand Down Expand Up @@ -27,17 +30,61 @@ export async function createReferral(
return referral;
}

const INACTIVE_REFERRAL_STATUSES = ["Leased", "Denied", "Canceled"];

export async function editReferral(
id: string,
assignedHousingLocatorId: string,
assignedReferringStaffId: string,
status: string,
) {
const Ref = await ReferralModel.findById(id);
const updateHL = Ref?.assignedHousingLocator?.toString() !== assignedHousingLocatorId;
const setLeased =
status === "Leased" &&
Ref?.status !== "Leased" &&
Ref?.status !== "Denied" &&
Ref?.status !== "Canceled";
await ReferralModel.findByIdAndUpdate(id, {
assignedHousingLocator: assignedHousingLocatorId,
assignedReferringStaff: assignedReferringStaffId,
status,
});
const referral = await ReferralModel.findById(id);
const HL = await getUserByID(referral?.assignedHousingLocator?.toString() ?? "");

if (updateHL) {
if (HL !== null) {
await sendEmail(
HL.email,
"Referral Update",
`You have been assigned a new referral. Please login to the portal to view the update.`,
);
}
}
if (setLeased) {
const refs = await getUnitReferrals(referral?.unit.toString() ?? "");
const userPromises = [];
const emailPromises = [];
for (const ref of refs) {
if (!INACTIVE_REFERRAL_STATUSES.includes(ref.status)) {
userPromises.push(getUserByID(ref.assignedHousingLocator?.toString() ?? ""));
}
}
const users = await Promise.all(userPromises);
for (const user of users) {
if (user?.email) {
emailPromises.push(
sendEmail(user.email, "Referral Update", "One of your referrals has been leased."),
);
}
}
await Promise.all(emailPromises);
}

return referral;
}

export async function deleteReferral(id: string) {
return await ReferralModel.deleteOne({ _id: id });
}
8 changes: 7 additions & 1 deletion backend/src/services/renter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ReferralModel } from "../models/referral";
import { RenterModel } from "../models/renter";
import { Renter, RenterModel } from "../models/renter";

export type EditRenterCandidateBody = Partial<Renter>;

//Fetch renters from DB
export async function getRenterCandidates() {
Expand Down Expand Up @@ -36,3 +38,7 @@ export async function createRenterCandidate(
return null;
}
}

export async function editRenterCandidate(id: string, editQuery: EditRenterCandidateBody) {
return await RenterModel.findByIdAndUpdate(id, editQuery, { new: true });
}
105 changes: 100 additions & 5 deletions backend/src/services/units.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UpdateQuery } from "mongoose";
import { FilterQuery, UpdateQuery } from "mongoose";

import { Unit, UnitModel } from "@/models/units";

Expand All @@ -24,11 +24,23 @@ export type EditUnitBody = { dateAvailable: string } & Omit<Unit, UserReadOnlyFi
export type FilterParams = {
search?: string;
availability?: string;
housingAuthority?: string;
accessibility?: string;
rentalCriteria?: string;
additionalRules?: string;
minPrice?: string;
maxPrice?: string;
minSecurityDeposit?: string;
maxSecurityDeposit?: string;
minApplicationFee?: string;
maxApplicationFee?: string;
minSize?: string;
maxSize?: string;
fromDate?: string;
toDate?: string;
beds?: string;
baths?: string;
sort?: string;
minPrice?: string;
maxPrice?: string;
approved?: "pending" | "approved";
};

Expand Down Expand Up @@ -90,6 +102,23 @@ export const getUnits = async (filters: FilterParams) => {
const minPrice = filters.minPrice === "undefined" ? 0 : +(filters.minPrice ?? 0);
const maxPrice = filters.maxPrice === "undefined" ? 100000 : +(filters.maxPrice ?? 100000);

const minSecurityDeposit =
filters.minSecurityDeposit === "undefined" ? 0 : +(filters.minSecurityDeposit ?? 0);
const maxSecurityDeposit =
filters.maxSecurityDeposit === "undefined" ? 100000 : +(filters.maxSecurityDeposit ?? 100000);

const minApplicationFee =
filters.minApplicationFee === "undefined" ? 0 : +(filters.minApplicationFee ?? 0);
const maxApplicationFee =
filters.maxApplicationFee === "undefined" ? 100000 : +(filters.maxApplicationFee ?? 100000);

const minSize = filters.minSize === "undefined" ? 0 : +(filters.minSize ?? 0);
const maxSize = filters.maxSize === "undefined" ? 100000 : +(filters.maxSize ?? 100000);

const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
const fromDate = dateRegex.test(filters.fromDate ?? "") ? filters.fromDate : "1900-01-01";
const toDate = dateRegex.test(filters.toDate ?? "") ? filters.toDate : "2100-01-01";

const avail = filters.availability ? (filters.availability === "Available" ? true : false) : true;
const approved = filters.approved ? (filters.approved === "approved" ? true : false) : true;

Expand All @@ -115,12 +144,78 @@ export const getUnits = async (filters: FilterParams) => {
break;
}

const units = await UnitModel.find({
const accessibilityCheckboxMap = new Map<string, string>([
["First Floor", "1st floor"],
["> Second Floor", "2nd floor and above"],
["Ramps", "Ramps up to unit"],
["Stairs Only", "Stairs only"],
["Elevators", "Elevators to unit"],
]);

const rentalCriteriaCheckboxMap = new Map<string, string>([
["3rd Party Payment", "3rd party payment accepting"],
["Credit Check Required", "Credit check required"],
["Background Check Required", "Background check required"],
["Program Letter Required", "Program letter required"],
]);

const additionalRulesCheckboxMap = new Map<string, string>([
["Pets Allowed", "Pets allowed"],
["Manager On Site", "Manager on site"],
["Quiet Building", "Quiet Building"],
["Visitor Policies", "Visitor Policies"],
["Kid Friendly", "Kid friendly"],
["Min-management Interaction", "Minimal-management interaction"],
["High-management Interaction", "High-management interaction"],
]);

const hasHousingAuthority = filters.housingAuthority !== "Any";
const hasAccessibility = !(filters.accessibility === undefined || filters.accessibility === "[]");
const rentalCriteria = !(filters.rentalCriteria === undefined || filters.rentalCriteria === "[]");
const additionalRules = !(
filters.additionalRules === undefined || filters.additionalRules === "[]"
);

const query: FilterQuery<Unit> = {
numBeds: { $gte: filters.beds ?? 1 },
numBaths: { $gte: filters.baths ?? 0.5 },
monthlyRent: { $gte: minPrice, $lte: maxPrice },
securityDeposit: { $gte: minSecurityDeposit, $lte: maxSecurityDeposit },
applicationFeeCost: { $gte: minApplicationFee, $lte: maxApplicationFee },
sqft: { $gte: minSize, $lte: maxSize },
dateAvailable: { $gte: fromDate, $lte: toDate },
approved,
}).sort(sortingCriteria);
};

if (hasHousingAuthority) {
query.housingAuthority = filters.housingAuthority ?? { $exists: true };
}

if (hasAccessibility) {
query.accessibility = {
$in: (JSON.parse(filters.accessibility ?? "[]") as string[]).map((str: string) =>
accessibilityCheckboxMap.get(str),
) as string[],
};
}

if (rentalCriteria) {
query.paymentRentingCriteria = {
$in: (JSON.parse(filters.rentalCriteria ?? "[]") as string[]).map((str: string) =>
rentalCriteriaCheckboxMap.get(str),
) as string[],
};
}

if (additionalRules) {
query.additionalRules = {
$in: (JSON.parse(filters.additionalRules ?? "[]") as string[]).map((str: string) =>
additionalRulesCheckboxMap.get(str),
) as string[],
};
}

const units = await UnitModel.find(query).sort(sortingCriteria);

const filteredUnits = units.filter((unit: Unit) => {
return addressRegex.test(unit.listingAddress) && unit.availableNow === avail;
Expand Down
Loading

0 comments on commit a6a1c9a

Please sign in to comment.