Skip to content

Commit

Permalink
fix: remove ability to edit close and lottery dates on a published li…
Browse files Browse the repository at this point in the history
…sting (#623)
  • Loading branch information
KrissDrawing authored Jul 3, 2024
1 parent 8d6c02e commit b252458
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 12 deletions.
33 changes: 31 additions & 2 deletions api/src/services/listing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ConfigService } from '@nestjs/config';
import { SchedulerRegistry } from '@nestjs/schedule';
import {
LanguagesEnum,
ListingEventsTypeEnum,
ListingsStatusEnum,
Prisma,
ReviewOrderTypeEnum,
Expand Down Expand Up @@ -48,6 +49,7 @@ import { startCronJob } from '../utilities/cron-job-starter';
import { PermissionService } from './permission.service';
import { permissionActions } from '../enums/permissions/permission-actions-enum';
import Unit from '../dtos/units/unit.dto';
import { checkIfDatesChanged } from '../utilities/listings-utilities';

export type getListingsArgs = {
skip: number;
Expand Down Expand Up @@ -1152,6 +1154,8 @@ export class ListingService implements OnModuleInit {
*/
async update(dto: ListingUpdate, requestingUser: User): Promise<Listing> {
const storedListing = await this.findOrThrow(dto.id, ListingViews.details);
const isNonAdmin = !requestingUser?.userRoles?.isAdmin;
const isActiveListing = dto.status === ListingsStatusEnum.active;

await this.permissionService.canOrThrow(
requestingUser,
Expand All @@ -1163,6 +1167,31 @@ export class ListingService implements OnModuleInit {
},
);

//check if the user has permission to edit dates
if (isNonAdmin && isActiveListing) {
const lotteryEvent = dto.listingEvents?.find(
(event) => event?.type === ListingEventsTypeEnum.publicLottery,
);
const storedLotteryEvent = storedListing.listingEvents?.find(
(event) => event?.type === ListingEventsTypeEnum.publicLottery,
);

if (
checkIfDatesChanged(
lotteryEvent,
storedLotteryEvent,
dto,
storedListing.applicationDueDate?.toISOString(),
storedListing.reviewOrderType,
)
) {
throw new HttpException(
'You do not have permission to edit dates',
403,
);
}
}

dto.unitsAvailable =
dto.reviewOrderType !== ReviewOrderTypeEnum.waitlist && dto.units
? dto.units.length
Expand Down Expand Up @@ -1600,7 +1629,7 @@ export class ListingService implements OnModuleInit {
clears the listing cache of either 1 listing or all listings
@param storedListingStatus the status that was stored for the listing
@param incomingListingStatus the incoming "new" status for a listing
@param savedResponseId the id of the listing
@param savedResponseId the id of the listing
*/
async cachePurge(
storedListingStatus: ListingsStatusEnum | undefined,
Expand Down Expand Up @@ -1724,7 +1753,7 @@ export class ListingService implements OnModuleInit {

/**
marks the db record for this cronjob as begun or creates a cronjob that
is marked as begun if one does not already exist
is marked as begun if one does not already exist
*/
async markCronJobAsStarted(): Promise<void> {
const job = await this.prisma.cronJob.findFirst({
Expand Down
39 changes: 39 additions & 0 deletions api/src/utilities/listings-utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ListingEventsTypeEnum } from '@prisma/client';
import { ListingUpdate } from '../dtos/listings/listing-update.dto';
import { ListingEventCreate } from '../dtos/listings/listing-event-create.dto';
import { ListingEvent } from '../dtos/listings/listing-event.dto';

export const checkIfDatesChanged = (
lotteryEvent: ListingEventCreate,
storedLotteryEvent: ListingEvent,
dto: ListingUpdate,
storedApplicationDueDate: string,
storedReviewOrderType: string,
) => {
const isPublicLotteryEvent =
lotteryEvent?.type === ListingEventsTypeEnum.publicLottery;

const isSameStartDate =
lotteryEvent?.startDate?.toISOString() ===
storedLotteryEvent?.startDate?.toISOString();
const isSameStartTime =
lotteryEvent?.startTime?.toISOString() ===
storedLotteryEvent?.startTime?.toISOString();
const isSameEndTime =
lotteryEvent?.endTime?.toISOString() ===
storedLotteryEvent?.endTime?.toISOString();

const isDifferentReviewOrderType =
storedReviewOrderType !== dto?.reviewOrderType;
const isDifferentApplicationDueDate =
dto.applicationDueDate?.toISOString() !== storedApplicationDueDate;
const isDifferentLotteryEventTimes =
isPublicLotteryEvent &&
!(isSameStartDate && isSameStartTime && isSameEndTime);

return (
isDifferentReviewOrderType ||
isDifferentLotteryEventTimes ||
isDifferentApplicationDueDate
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,8 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr
});

const val = await constructFullListingData(prisma, listing.id, jurisId);
val.applicationDueDate = listing.applicationDueDate;
val.reviewOrderType = listing.reviewOrderType;

await request(app.getHttpServer())
.put(`/listings/${listing.id}`)
Expand Down Expand Up @@ -1300,7 +1302,7 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr

it('should succeed for process endpoint', async () => {
/*
Because so many different iterations of the process endpoint were firing we were running into collisions.
Because so many different iterations of the process endpoint were firing we were running into collisions.
Since this is just testing the permissioning aspect I'm switching to mocking the process function
*/
applicationFlaggedSetService.process = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,8 @@ describe('Testing Permissioning of endpoints as partner with correct listing', (
userListingId,
jurisId,
);
val.applicationDueDate = new Date('05-16-2024 01:25:18PM GMT+2');
val.reviewOrderType = null;

await request(app.getHttpServer())
.put(`/listings/${userListingId}`)
Expand Down Expand Up @@ -1213,7 +1215,7 @@ describe('Testing Permissioning of endpoints as partner with correct listing', (

it('should succeed for process endpoint', async () => {
/*
Because so many different iterations of the process endpoint were firing we were running into collisions.
Because so many different iterations of the process endpoint were firing we were running into collisions.
Since this is just testing the permissioning aspect I'm switching to mocking the process function
*/
applicationFlaggedSetService.process = jest.fn();
Expand Down
6 changes: 4 additions & 2 deletions api/test/unit/services/listing.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import { permissionActions } from '../../../src/enums/permissions/permission-act
import { disconnect } from 'process';

/*
generates a super simple mock listing for us to test logic with
*/
generates a super simple mock listing for us to test logic with
*/
const mockListing = (
pos: number,
genUnits?: { numberToMake: number; date: Date },
Expand Down Expand Up @@ -2407,6 +2407,8 @@ describe('Testing listing service', () => {

const val = constructFullListingData(randomUUID());
val.reservedCommunityTypes = null;
val.applicationDueDate = undefined;
val.reviewOrderType = undefined;

await service.update(val as ListingUpdate, user);

Expand Down
12 changes: 10 additions & 2 deletions sites/partners/src/components/listings/PaperListingForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type ListingFormProps = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ListingForm = ({ listing, editMode }: ListingFormProps) => {
const defaultValues = editMode ? listing : formDefaults
const isListingActive = listing?.status === ListingsStatusEnum.active
const formMethods = useForm<FormListing>({
defaultValues,
shouldUnregister: false,
Expand Down Expand Up @@ -147,7 +148,7 @@ const ListingForm = ({ listing, editMode }: ListingFormProps) => {
newData?: Partial<FormListing>
) => {
if (confirm && status === ListingsStatusEnum.active) {
if (listing?.status === ListingsStatusEnum.active) {
if (isListingActive) {
setListingIsAlreadyLiveModal(true)
} else {
setPublishModal(true)
Expand Down Expand Up @@ -314,6 +315,9 @@ const ListingForm = ({ listing, editMode }: ListingFormProps) => {
units={units}
setUnits={setUnits}
disableUnitsAccordion={listing?.disableUnitsAccordion}
disableListingAvailability={
isListingActive && !profile.userRoles.isAdmin
}
/>
<SelectAndOrder
addText={t("listings.addPreference")}
Expand Down Expand Up @@ -370,14 +374,18 @@ const ListingForm = ({ listing, editMode }: ListingFormProps) => {
</div>
</Tabs.TabPanel>
<Tabs.TabPanel>
<RankingsAndResults listing={listing} />
<RankingsAndResults
listing={listing}
disableDueDates={isListingActive && !profile.userRoles.isAdmin}
/>
<LeasingAgent />
<ApplicationTypes listing={listing} />
<ApplicationAddress listing={listing} />
<ApplicationDates
listing={listing}
openHouseEvents={openHouseEvents}
setOpenHouseEvents={setOpenHouseEvents}
disableDueDate={isListingActive && !profile.userRoles.isAdmin}
/>

<div className="-ml-8 -mt-8 relative" style={{ top: "7rem" }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ type ApplicationDatesProps = {
openHouseEvents: TempEvent[]
setOpenHouseEvents: (events: TempEvent[]) => void
listing?: FormListing
disableDueDate?: boolean
}

const ApplicationDates = ({
listing,
openHouseEvents,
setOpenHouseEvents,
disableDueDate,
}: ApplicationDatesProps) => {
const openHouseHeaders = {
date: "t.date",
Expand Down Expand Up @@ -116,7 +118,7 @@ const ApplicationDates = ({
register={register}
watch={watch}
note={t("listings.whenApplicationsClose")}
disabled={enableDueDate === YesNoEnum.no}
disabled={disableDueDate || enableDueDate === YesNoEnum.no}
defaultDate={{
month: listing?.applicationDueDate
? dayjs(new Date(listing?.applicationDueDate)).format("MM")
Expand All @@ -137,7 +139,7 @@ const ApplicationDates = ({
id={"applicationDueTimeField"}
register={register}
watch={watch}
disabled={enableDueDate === YesNoEnum.no}
disabled={disableDueDate || enableDueDate === YesNoEnum.no}
defaultValues={{
hours: listing?.applicationDueDate
? dayjs(new Date(listing?.applicationDueDate)).format("hh")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import SectionWithGrid from "../../../shared/SectionWithGrid"

type RankingsAndResultsProps = {
listing?: FormListing
disableDueDates?: boolean
}

const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => {
const RankingsAndResults = ({ listing, disableDueDates }: RankingsAndResultsProps) => {
const formMethods = useFormContext()

// eslint-disable-next-line @typescript-eslint/unbound-method
Expand Down Expand Up @@ -79,13 +80,18 @@ const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => {
label: t("listings.firstComeFirstServe"),
value: "reviewOrderFCFS",
id: "reviewOrderFCFS",
disabled:
disableDueDates && listing?.reviewOrderType === ReviewOrderTypeEnum.lottery,
defaultChecked:
listing?.reviewOrderType === ReviewOrderTypeEnum.firstComeFirstServe,
},
{
label: t("listings.lotteryTitle"),
value: "reviewOrderLottery",
id: "reviewOrderLottery",
disabled:
disableDueDates &&
listing?.reviewOrderType === ReviewOrderTypeEnum.firstComeFirstServe,
defaultChecked: listing?.reviewOrderType === ReviewOrderTypeEnum.lottery,
},
]}
Expand All @@ -105,11 +111,13 @@ const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => {
{
...yesNoRadioOptions[0],
id: "dueDateQuestionYes",
disabled: disableDueDates && !listing?.applicationDueDate,
defaultChecked: listing && listing.applicationDueDate !== null,
},
{
...yesNoRadioOptions[1],
id: "dueDateQuestionNo",
disabled: disableDueDates && listing?.applicationDueDate !== null,
defaultChecked: listing && !listing.applicationDueDate,
},
]}
Expand All @@ -127,6 +135,7 @@ const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => {
id={"lotteryDate"}
register={register}
watch={watch}
disabled={disableDueDates}
defaultDate={{
month: lotteryEvent?.startDate
? dayjs(new Date(lotteryEvent?.startDate)).utc().format("MM")
Expand All @@ -147,6 +156,7 @@ const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => {
id={"lotteryStartTime"}
register={register}
watch={watch}
disabled={disableDueDates}
defaultValues={{
hours: lotteryEvent?.startTime
? dayjs(new Date(lotteryEvent?.startTime)).format("hh")
Expand All @@ -168,6 +178,7 @@ const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => {
id={"lotteryEndTime"}
register={register}
watch={watch}
disabled={disableDueDates}
defaultValues={{
hours: lotteryEvent?.endTime
? dayjs(new Date(lotteryEvent?.endTime)).format("hh")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ type UnitProps = {
units: TempUnit[]
setUnits: (units: TempUnit[]) => void
disableUnitsAccordion: boolean
disableListingAvailability?: boolean
}

const FormUnits = ({ units, setUnits, disableUnitsAccordion }: UnitProps) => {
const FormUnits = ({
units,
setUnits,
disableUnitsAccordion,
disableListingAvailability,
}: UnitProps) => {
const [unitDrawerOpen, setUnitDrawerOpen] = useState(false)
const [unitDeleteModal, setUnitDeleteModal] = useState<number | null>(null)
const [defaultUnit, setDefaultUnit] = useState<TempUnit | null>(null)
Expand Down Expand Up @@ -185,13 +191,19 @@ const FormUnits = ({ units, setUnits, disableUnitsAccordion }: UnitProps) => {
id: "availableUnits",
dataTestId: "listingAvailability.availableUnits",
defaultChecked: listing?.reviewOrderType !== ReviewOrderTypeEnum.waitlist,
disabled:
disableListingAvailability &&
listing?.reviewOrderType === ReviewOrderTypeEnum.waitlist,
},
{
label: t("listings.waitlist.open"),
value: "openWaitlist",
id: "openWaitlist",
dataTestId: "listingAvailability.openWaitlist",
defaultChecked: listing?.reviewOrderType === ReviewOrderTypeEnum.waitlist,
disabled:
disableListingAvailability &&
listing?.reviewOrderType !== ReviewOrderTypeEnum.waitlist,
},
]}
/>
Expand Down

0 comments on commit b252458

Please sign in to comment.