Skip to content

Commit

Permalink
feat: bulk app resend script runner (#4111)
Browse files Browse the repository at this point in the history
* feat: bulk app resend script runner

* fix: updates

* fix: forgot swagger
  • Loading branch information
YazeedLoonat committed May 31, 2024
1 parent a2eba57 commit 34794db
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 6 deletions.
10 changes: 10 additions & 0 deletions api/prisma/seed-helpers/translation-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ const translations = (jurisdictionName?: string, language?: LanguagesEnum) => {
'Use the following code to sign in to your %{jurisdictionName} account. This code will be valid for 5 minutes. Never share this code.',
singleUseCode: '%{singleUseCode}',
},
scriptRunner: {
information:
'You previously applied to Fremont Family Apartments, but an error resulted in sending your confirmation email without your confirmation number. Your application number and confirmation number are re-sent below. Thank you for your patience.',
pleaseSave: 'Please save this email for your records.',
subject:
'Your Application Confirmation Number for Fremont Family Apartments',
yourApplicationNumber: 'Your application number is: %{id}',
yourConfirmationNumber:
'Your confirmation number is: %{confirmationCode}',
},
};
} else if (language === LanguagesEnum.es) {
return {
Expand Down
18 changes: 18 additions & 0 deletions api/src/controllers/script-runner.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SuccessDTO } from '../dtos/shared/success.dto';
import { OptionalAuthGuard } from '../guards/optional.guard';
import { AdminOrJurisdictionalAdminGuard } from '../guards/admin-or-jurisdiction-admin.guard';
import { DataTransferDTO } from '../dtos/script-runner/data-transfer.dto';
import { BulkApplicationResendDTO } from '../dtos/script-runner/bulk-application-resend.dto';

@Controller('scriptRunner')
@ApiTags('scriptRunner')
Expand Down Expand Up @@ -45,4 +46,21 @@ export class ScirptRunnerController {
): Promise<SuccessDTO> {
return await this.scriptRunnerService.dataTransfer(req, dataTransferDTO);
}

@Put('bulkApplicationResend')
@ApiOperation({
summary:
'A script that resends application confirmations to applicants of a listing',
operationId: 'bulkApplicationResend',
})
@ApiOkResponse({ type: SuccessDTO })
async bulkApplicationResend(
@Body() bulkApplicationResendDTO: BulkApplicationResendDTO,
@Request() req: ExpressRequest,
): Promise<SuccessDTO> {
return await this.scriptRunnerService.bulkApplicationResend(
req,
bulkApplicationResendDTO,
);
}
}
13 changes: 13 additions & 0 deletions api/src/dtos/script-runner/bulk-application-resend.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsDefined, IsString, IsUUID } from 'class-validator';
import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum';

export class BulkApplicationResendDTO {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@IsUUID(4, { groups: [ValidationsGroupsEnum.default] })
@ApiProperty()
listingId: string;
}
3 changes: 2 additions & 1 deletion api/src/modules/script-runner.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { ScirptRunnerController } from '../controllers/script-runner.controller'
import { ScriptRunnerService } from '../services/script-runner.service';
import { PrismaModule } from './prisma.module';
import { PermissionModule } from './permission.module';
import { EmailModule } from './email.module';

@Module({
imports: [PrismaModule, PermissionModule],
imports: [PrismaModule, PermissionModule, EmailModule],
controllers: [ScirptRunnerController],
providers: [ScriptRunnerService],
exports: [ScriptRunnerService],
Expand Down
83 changes: 82 additions & 1 deletion api/src/services/script-runner.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ import { SuccessDTO } from '../dtos/shared/success.dto';
import { User } from '../dtos/users/user.dto';
import { mapTo } from '../utilities/mapTo';
import { DataTransferDTO } from '../dtos/script-runner/data-transfer.dto';
import { BulkApplicationResendDTO } from '../dtos/script-runner/bulk-application-resend.dto';
import { EmailService } from './email.service';
import { Application } from '../dtos/applications/application.dto';

/**
this is the service for running scripts
most functions in here will be unique, but each function should only be allowed to fire once
*/
@Injectable()
export class ScriptRunnerService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private emailService: EmailService,
) {}

/**
*
Expand Down Expand Up @@ -53,6 +59,81 @@ export class ScriptRunnerService {
return { success: true };
}

/**
*
* @param req incoming request object
* @param bulkApplicationResendDTO bulk resend arg. Should contain listing id
* @returns successDTO
* @description resends a confirmation email to all applicants on a listing with an email
*/
async bulkApplicationResend(
req: ExpressRequest,
bulkApplicationResendDTO: BulkApplicationResendDTO,
): Promise<SuccessDTO> {
// script runner standard start up
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart('bulk application resend', requestingUser);

// gather listing data
const listing = await this.prisma.listings.findUnique({
select: {
id: true,
jurisdictions: {
select: {
id: true,
},
},
},
where: {
id: bulkApplicationResendDTO.listingId,
},
});

if (!listing || !listing.jurisdictions) {
throw new BadRequestException('Listing does not exist');
}

// gather up all applications for that listing
const rawApplications = await this.prisma.applications.findMany({
select: {
id: true,
language: true,
confirmationCode: true,
applicant: {
select: {
id: true,
emailAddress: true,
firstName: true,
middleName: true,
lastName: true,
},
},
},
where: {
listingId: bulkApplicationResendDTO.listingId,
deletedAt: null,
applicant: {
emailAddress: {
not: null,
},
},
},
});
const applications = mapTo(Application, rawApplications);

// send emails
for (const application of applications) {
await this.emailService.applicationScriptRunner(
mapTo(Application, application),
{ id: listing.jurisdictions.id },
);
}

// script runner standard spin down
await this.markScriptAsComplete('bulk application resend', requestingUser);
return { success: true };
}

/**
this is simply an example
*/
Expand Down
133 changes: 129 additions & 4 deletions api/test/unit/services/script-runner.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { randomUUID } from 'crypto';
import { Request as ExpressRequest } from 'express';
import { ScriptRunnerService } from '../../../src/services/script-runner.service';
import { PrismaService } from '../../../src/services/prisma.service';
import { User } from '../../../src/dtos/users/user.dto';
import { EmailService } from '../../../src/services/email.service';

describe('Testing script runner service', () => {
let service: ScriptRunnerService;
let prisma: PrismaService;
let emailService: EmailService;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ScriptRunnerService,
PrismaService,
Logger,
SchedulerRegistry,
{
provide: EmailService,
useValue: {
applicationScriptRunner: jest.fn(),
},
},
],
}).compile();

service = module.get<ScriptRunnerService>(ScriptRunnerService);
emailService = module.get<EmailService>(EmailService);
prisma = module.get<PrismaService>(PrismaService);
});

Expand Down Expand Up @@ -67,6 +73,125 @@ describe('Testing script runner service', () => {
});
});

it('should bulk resend application confirmations', async () => {
const id = randomUUID();
const scriptName = 'bulk application resend';
const listingId = randomUUID();
const jurisdictionId = randomUUID();
const applicationId = randomUUID();

prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.create = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.update = jest.fn().mockResolvedValue(null);
prisma.listings.findUnique = jest.fn().mockResolvedValue({
id: listingId,
jurisdictions: {
id: jurisdictionId,
},
});
prisma.applications.findMany = jest.fn().mockResolvedValue([
{
id: applicationId,
language: 'en',
confirmationCode: 'conf code',
applicant: {
id: randomUUID(),
emailAddress: 'example email address',
firstName: 'example first name',
middleName: 'example middle name',
lastName: 'example last name',
},
},
]);

const res = await service.bulkApplicationResend(
{
user: {
id,
} as unknown as User,
} as unknown as ExpressRequest,
{
listingId,
},
);

expect(res.success).toBe(true);

expect(prisma.scriptRuns.findUnique).toHaveBeenCalledWith({
where: {
scriptName,
},
});
expect(prisma.scriptRuns.create).toHaveBeenCalledWith({
data: {
scriptName,
triggeringUser: id,
},
});
expect(prisma.scriptRuns.update).toHaveBeenCalledWith({
data: {
didScriptRun: true,
triggeringUser: id,
},
where: {
scriptName,
},
});
expect(prisma.listings.findUnique).toHaveBeenCalledWith({
select: {
id: true,
jurisdictions: {
select: {
id: true,
},
},
},
where: {
id: listingId,
},
});
expect(prisma.applications.findMany).toHaveBeenCalledWith({
select: {
id: true,
language: true,
confirmationCode: true,
applicant: {
select: {
id: true,
emailAddress: true,
firstName: true,
middleName: true,
lastName: true,
},
},
},
where: {
listingId: listingId,
deletedAt: null,
applicant: {
emailAddress: {
not: null,
},
},
},
});
expect(emailService.applicationScriptRunner).toHaveBeenCalledWith(
{
id: applicationId,
language: 'en',
confirmationCode: 'conf code',
applicant: {
id: expect.anything(),
emailAddress: 'example email address',
firstName: 'example first name',
middleName: 'example middle name',
lastName: 'example last name',
},
},
{ id: jurisdictionId },
);
});

// | ---------- HELPER TESTS BELOW ---------- | //
it('should mark script run as started if no script run present in db', async () => {
prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue(null);
Expand Down
27 changes: 27 additions & 0 deletions shared-helpers/src/types/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,28 @@ export class ScriptRunnerService {

configs.data = data

axios(configs, resolve, reject)
})
}
/**
* A script that resends application confirmations to applicants of a listing
*/
bulkApplicationResend(
params: {
/** requestBody */
body?: BulkApplicationResendDTO
} = {} as any,
options: IRequestOptions = {}
): Promise<SuccessDTO> {
return new Promise((resolve, reject) => {
let url = basePath + "/scriptRunner/bulkApplicationResend"

const configs: IRequestConfig = getConfigs("put", "application/json", url, options)

let data = params.body

configs.data = data

axios(configs, resolve, reject)
})
}
Expand Down Expand Up @@ -5226,6 +5248,11 @@ export interface DataTransferDTO {
connectionString: string
}

export interface BulkApplicationResendDTO {
/** */
listingId: string
}

export enum ListingViews {
"fundamentals" = "fundamentals",
"base" = "base",
Expand Down

0 comments on commit 34794db

Please sign in to comment.