Skip to content

Commit

Permalink
Merge branch 'main' into 4300/publish-lottery-bug
Browse files Browse the repository at this point in the history
  • Loading branch information
ColinBuyck authored Sep 13, 2024
2 parents b717e1f + ceed376 commit 3e78c3a
Show file tree
Hide file tree
Showing 20 changed files with 547 additions and 45 deletions.
18 changes: 18 additions & 0 deletions api/prisma/migrations/16_lottery_total/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "application_lottery_totals" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"total" INTEGER NOT NULL,
"listing_id" UUID NOT NULL,
"multiselect_question_id" UUID,

CONSTRAINT "application_lottery_totals_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "application_lottery_totals_listing_id_idx" ON "application_lottery_totals"("listing_id");

-- AddForeignKey
ALTER TABLE "application_lottery_totals" ADD CONSTRAINT "application_lottery_totals_listing_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "listings"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

-- AddForeignKey
ALTER TABLE "application_lottery_totals" ADD CONSTRAINT "application_lottery_totals_multiselect_question_id_fkey" FOREIGN KEY ("multiselect_question_id") REFERENCES "multiselect_questions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
15 changes: 14 additions & 1 deletion api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,18 @@ model ApplicationLotteryPositions {
@@map("application_lottery_positions")
}

model ApplicationLotteryTotal {
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
total Int
listingId String @map("listing_id") @db.Uuid
listings Listings @relation(fields: [listingId], references: [id], onDelete: Cascade, onUpdate: NoAction)
multiselectQuestionId String? @map("multiselect_question_id") @db.Uuid
multiselectQuestions MultiselectQuestions? @relation(fields: [multiselectQuestionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@index([listingId])
@@map("application_lottery_totals")
}

model ListingUtilities {
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
Expand Down Expand Up @@ -592,7 +604,7 @@ model Listings {
requestedChangesUserId String? @map("requested_changes_user_id") @db.Uuid
requestedChangesUser UserAccounts? @relation("requested_changes_user", fields: [requestedChangesUserId], references: [id], onDelete: NoAction, onUpdate: NoAction)
applicationLotteryPositions ApplicationLotteryPositions[]
applicationLotteryTotals ApplicationLotteryTotal[]
@@index([jurisdictionId])
@@map("listings")
}
Expand Down Expand Up @@ -632,6 +644,7 @@ model MultiselectQuestions {
jurisdictions Jurisdictions[]
listings ListingMultiselectQuestions[]
applicationLotteryPositions ApplicationLotteryPositions[]
applicationLotteryTotals ApplicationLotteryTotal[]
@@map("multiselect_questions")
}
Expand Down
17 changes: 17 additions & 0 deletions api/src/controllers/lottery.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { PermissionAction } from '../../src/decorators/permission-action.decorat
import { permissionActions } from '../../src/enums/permissions/permission-actions-enum';
import { AdminOrJurisdictionalAdminGuard } from '../../src/guards/admin-or-jurisdiction-admin.guard';
import { PublicLotteryResult } from '../../src/dtos/lottery/lottery-public-result.dto';
import { PublicLotteryTotal } from '../../src/dtos/lottery/lottery-public-total.dto';

@Controller('lottery')
@ApiTags('lottery')
Expand Down Expand Up @@ -163,4 +164,20 @@ export class LotteryController {
mapTo(User, req['user']),
);
}

@Get(`lotteryTotals/:id`)
@ApiOkResponse({
type: PublicLotteryTotal,
isArray: true,
})
@ApiOperation({
summary: 'Get lottery totals by listing id',
operationId: 'lotteryTotals',
})
async lotteryTotals(
@Request() req: ExpressRequest,
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
): Promise<PublicLotteryTotal[]> {
return this.lotteryService.lotteryTotals(id, mapTo(User, req['user']));
}
}
12 changes: 9 additions & 3 deletions api/src/dtos/applications/application-lottery-position.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsDefined, IsNumber, IsString, IsUUID } from 'class-validator';
import {
IsDefined,
IsNumber,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class ApplicationLotteryPosition {
Expand All @@ -21,9 +27,9 @@ export class ApplicationLotteryPosition {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
multiselectQuestionId: string;
multiselectQuestionId?: string;

@Expose()
@IsNumber({}, { groups: [ValidationsGroupsEnum.default] })
Expand Down
32 changes: 32 additions & 0 deletions api/src/dtos/applications/application-lottery-total.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import {
IsDefined,
IsNumber,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class ApplicationLotteryTotal {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
listingId: string;

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@ApiPropertyOptional()
multiselectQuestionId?: string;

@Expose()
@IsNumber({}, { groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
total: number;
}
1 change: 1 addition & 0 deletions api/src/dtos/listings/listing-update.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class ListingUpdate extends OmitType(Listing, [
'afsLastRunAt',
'urlSlug',
'applicationConfig',
'applicationLotteryTotals',
]) {
@Expose()
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
Expand Down
10 changes: 10 additions & 0 deletions api/src/dtos/listings/listing.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Expose, Transform, TransformFnParams, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDate,
IsDefined,
Expand Down Expand Up @@ -39,6 +40,7 @@ import { listingUrlSlug } from '../../utilities/listing-url-slug';
import { User } from '../users/user.dto';
import { requestedChangesUserMapper } from '../../utilities/requested-changes-user';
import { LotteryDateParamValidator } from '../../utilities/lottery-date-validator';
import { ApplicationLotteryTotal } from '../applications/application-lottery-total.dto';

class Listing extends AbstractDTO {
@Expose()
Expand Down Expand Up @@ -603,6 +605,14 @@ class Listing extends AbstractDTO {
@IsBoolean({ groups: [ValidationsGroupsEnum.default] })
@ApiPropertyOptional()
lotteryOptIn?: boolean;

@Expose()
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => ApplicationLotteryTotal)
@ApiProperty({ type: ApplicationLotteryTotal, isArray: true })
applicationLotteryTotals: ApplicationLotteryTotal[];
}

export { Listing as default, Listing };
4 changes: 2 additions & 2 deletions api/src/dtos/lottery/lottery-public-result.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsDefined, IsNumber, IsOptional, IsUUID } from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';
Expand All @@ -11,7 +11,7 @@ export class PublicLotteryResult {
ordinal: number;

@Expose()
@ApiProperty()
@ApiPropertyOptional()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
multiselectQuestionId?: string;
Expand Down
18 changes: 18 additions & 0 deletions api/src/dtos/lottery/lottery-public-total.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsDefined, IsNumber, IsOptional, IsUUID } from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class PublicLotteryTotal {
@Expose()
@IsNumber({}, { groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
total: number;

@Expose()
@ApiPropertyOptional()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
multiselectQuestionId?: string;
}
95 changes: 81 additions & 14 deletions api/src/services/lottery.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { SchedulerRegistry } from '@nestjs/schedule';
import { ConfigService } from '@nestjs/config';
import {
ApplicationLotteryTotal,
ListingEventsTypeEnum,
ListingsStatusEnum,
LotteryStatusEnum,
Expand Down Expand Up @@ -48,6 +49,7 @@ import { ListingViews } from '../../src/enums/listings/view-enum';
import { startCronJob } from '../utilities/cron-job-starter';
import { EmailService } from './email.service';
import { PublicLotteryResult } from '../../src/dtos/lottery/lottery-public-result.dto';
import { PublicLotteryTotal } from '../../src/dtos/lottery/lottery-public-total.dto';

view.csv = {
...view.details,
Expand Down Expand Up @@ -136,6 +138,9 @@ export class LotteryService {
await this.prisma.applicationLotteryPositions.deleteMany({
where: { listingId: listingId },
});
await this.prisma.applicationLotteryTotal.deleteMany({
where: { listingId: listingId },
});
}

try {
Expand Down Expand Up @@ -241,6 +246,14 @@ export class LotteryService {
})),
});

await this.prisma.applicationLotteryTotal.create({
data: {
listingId,
total: filteredApplications.length,
multiselectQuestionId: null,
},
});

// order by ordinal
filteredApplications = filteredApplications.sort(
(a, b) =>
Expand Down Expand Up @@ -279,6 +292,13 @@ export class LotteryService {
multiselectQuestionId: id,
})),
});
await this.prisma.applicationLotteryTotal.create({
data: {
listingId,
total: applicationsWithThisPreference.length,
multiselectQuestionId: id,
},
});
}
}
}
Expand Down Expand Up @@ -1118,31 +1138,78 @@ export class LotteryService {
throw new ForbiddenException();
}

const applicationUserId = await this.prisma.applications.findFirstOrThrow({
if (!user.userRoles?.isAdmin) {
const applicationUserId = await this.prisma.applications.findFirst({
select: {
userId: true,
},
where: {
id: applicationId,
},
});

if (!applicationUserId) {
throw new BadRequestException(
`User requesting lottery results did not submit an application to this listing`,
);
}

await this.permissionService.canOrThrow(
user,
'application',
permissionActions.read,
{
userId: applicationUserId.userId,
},
);
}

const results = await this.prisma.applicationLotteryPositions.findMany({
select: {
userId: true,
ordinal: true,
multiselectQuestionId: true,
},
where: {
id: applicationId,
applicationId,
},
});

await this.permissionService.canOrThrow(
user,
'application',
permissionActions.read,
{
userId: applicationUserId.userId,
},
);
return results;
}

const results = await this.prisma.applicationLotteryPositions.findMany({
/*
* @param id - listing id
* @returns an array of totals
*/
public async lotteryTotals(
listingId: string,
user: User,
): Promise<PublicLotteryTotal[]> {
if (!user) {
throw new ForbiddenException();
}

if (!user.userRoles?.isAdmin) {
const application = await this.prisma.applications.findFirst({
where: {
listingId,
userId: user.id,
},
});
if (!application) {
throw new BadRequestException(
`User requesting lottery totals did not submit an application to this listing`,
);
}
}

const results = await this.prisma.applicationLotteryTotal.findMany({
select: {
ordinal: true,
total: true,
multiselectQuestionId: true,
},
where: {
applicationId,
listingId,
},
});

Expand Down
Loading

0 comments on commit 3e78c3a

Please sign in to comment.