Skip to content

Commit

Permalink
eoj complete
Browse files Browse the repository at this point in the history
  • Loading branch information
c1495616js committed Oct 26, 2024
1 parent 11d10f9 commit 4186498
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/applicant/applicant.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { SyncApplicantsAudit } from './entity/sync-applicants-audit.entity';
import { IENApplicantRecruiter } from './entity/ienapplicant-employee.entity';
import { IENApplicantActiveFlag } from './entity/ienapplicant-active-flag.entity';
import { Pathway } from './entity/pathway.entity';
import { EndOfJourneyService } from './endofjourney.service';

@Module({
imports: [
Expand Down Expand Up @@ -58,6 +59,7 @@ import { Pathway } from './entity/pathway.entity';
IENApplicantUtilService,
ExternalAPIService,
ExternalRequest,
EndOfJourneyService,
],
exports: [
IENApplicantService,
Expand Down
192 changes: 192 additions & 0 deletions apps/api/src/applicant/endofjourney.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Inject, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';

import { getConnection, EntityManager } from 'typeorm';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

import { STATUS } from '@ien/common';
import { AppLogger } from 'src/common/logger.service';
import { IENApplicantStatusAudit } from './entity/ienapplicant-status-audit.entity';
import { IENApplicantStatus } from './entity/ienapplicant-status.entity';

dayjs.extend(utc);
dayjs.extend(timezone);
const formatDateInPST = (date: Date) => {
return dayjs(date)
.tz('America/Los_Angeles') // Convert to PST
.format('YYYY-MM-DD'); // Format as YYYY-MM-DD
};

type Getter<T = any> = (manager: EntityManager) => Promise<T[]>;
type Setter<T = any> = (manager: EntityManager, list: T[]) => Promise<void>;
type IEN_APPLICANT_END_OF_JOURNEY = {
applicant_id: string;
effective_date: string;
status: string;
ha_pcn_id: string;
};

@Injectable()
export class EndOfJourneyService {
constructor(@Inject(Logger) private readonly logger: AppLogger) {}

/**
* Entry point
*/
async init(): Promise<void> {
this.logger.log(
`End of journey checking started at ${dayjs().tz('America/Los_Angeles')}`,
'END-OF-JOURNEY',
);

const queryRunner = getConnection().createQueryRunner();
await queryRunner.startTransaction();
const manager = queryRunner.manager;

try {
// handle end of journey: COMPLETED
await this.handleEndOfJourney(
this.getCompletedLists,
this.setCompletedLists,
manager,
STATUS.END_OF_JOURNEY_COMPLETE,
);

await manager.queryRunner?.commitTransaction();
this.logger.log(
`End of journey checking end at ${dayjs().tz('America/Los_Angeles')}`,
'END-OF-JOURNEY',
);
} catch (e) {
await manager.queryRunner?.rollbackTransaction();
if (e instanceof Error) {
throw new InternalServerErrorException(`Transaction failed: ${e.message}`);
} else {
throw new InternalServerErrorException('Transaction failed with an unknown error');
}
} finally {
await manager.queryRunner?.release();
}
}

async handleEndOfJourney(
getter: Getter,
setter: Setter,
manager: EntityManager,
status: STATUS,
): Promise<void> {
const list = await getter(manager);
if (list.length === 0) {
this.logger.log(
`End of journey checking status: ${status} at ${dayjs().tz(
'America/Los_Angeles',
)} with no data`,
'END-OF-JOURNEY',
);
return;
}
await setter(manager, list);
}

/**
* Checking for end of journey COMPLETED
* QUITERIA:
*/
getCompletedLists: Getter<IEN_APPLICANT_END_OF_JOURNEY> = async manager => {
const yesterday = dayjs().tz('America/Los_Angeles').subtract(1, 'day').toDate();
const oneYearBeforeYesterday = formatDateInPST(
dayjs(yesterday).tz('America/Los_Angeles').subtract(1, 'year').toDate(),
);

/**
* Query to fetch the latest status change information for applicants with a "Job Offer Accepted" status.
*
* @return
* - applicant_id: string, the unique identifier for each applicant.
* - effective_date: Date, the latest (most recent) effective_date related to the applicant's status.
* - status: string, the status of the applicant (in this case, "Job Offer Accepted").
*/
const query = manager
.createQueryBuilder(IENApplicantStatusAudit, 'audit')
.select('audit.applicant_id') // Select applicant_id
.addSelect("TO_CHAR(MAX(audit.effective_date), 'YYYY-MM-DD')", 'effective_date') // Format effective_date as YYYY-MM-DD
.addSelect('status.status', 'status') // Get the status
.addSelect('job.ha_pcn_id', 'ha_pcn_id')
.leftJoin('audit.status', 'status')
.leftJoin('audit.job', 'job')
.having('MAX(audit.effective_date) = :oneYearBeforeYesterday', { oneYearBeforeYesterday }) // Use HAVING for aggregate filtering
.where('status.status = :status', { status: STATUS.JOB_OFFER_ACCEPTED }) // Filter by status
.andWhere('audit.effective_date IS NOT NULL') // Filter out null effective_date
.groupBy('audit.applicant_id') // Group by applicant_id
.addGroupBy('status.id')
.addGroupBy('job.id');

const applicants = await query.getRawMany();

this.logger.log({ yesterday, oneYearBeforeYesterday, applicants }, 'END-OF-JOURNEY');
return applicants;
};
setCompletedLists: Setter<IEN_APPLICANT_END_OF_JOURNEY> = async (manager, list) => {
// write into the audit table with new milestone: END_OF_JOURNEY_COMPLETED
// start_date, notes, status

const today = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD');
for (const applicant of list) {
await manager
.createQueryBuilder()
.insert()
.into(IENApplicantStatusAudit)
.values({
applicant: { id: applicant.applicant_id }, // Setting the applicant_id from the list
start_date: today, // Start date is today in YYYY-MM-DD format
status: { status: STATUS.END_OF_JOURNEY_COMPLETE }, // Status is set to END_OF_JOURNEY_COMPLETED
notes: `Updated by Lambda CRON at ${dayjs()
.tz('America/Los_Angeles')
.format('YYYY-MM-DD HH:mm:ss')} and status: END_OF_JOURNEY_COMPLETE`, // Note with current time
})
.execute();
}

// write into ien_applicants_active_flag table with is_active = false
// Attempt to get the status ID, and handle the error if the status is not found
let endOfJourneyCompleteStatus;
try {
endOfJourneyCompleteStatus = await manager.findOneOrFail(IENApplicantStatus, {
where: { status: STATUS.END_OF_JOURNEY_COMPLETE },
});
} catch (error) {
this.logger.error(`Status not found: ${STATUS.END_OF_JOURNEY_COMPLETE}`, 'END-OF-JOURNEY');
throw new Error(`Status not found: ${STATUS.END_OF_JOURNEY_COMPLETE}`);
}
for (const applicant of list) {
// First, attempt the update
const result = await manager
.createQueryBuilder()
.update('ien_applicants_active_flag')
.set({
is_active: false,
status_id: endOfJourneyCompleteStatus.id,
})
.where('ha_id = :ha_pcn_id', { ha_pcn_id: applicant.ha_pcn_id })
.andWhere('applicant_id = :applicant_id', { applicant_id: applicant.applicant_id })
.execute();

// If no rows were updated, perform an insert
if (result.affected === 0) {
await manager
.createQueryBuilder()
.insert()
.into('ien_applicants_active_flag')
.values({
ha_id: applicant.ha_pcn_id,
applicant_id: applicant.applicant_id,
is_active: false,
status_id: endOfJourneyCompleteStatus.id,
})
.execute();
}
}
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { IENApplicant } from './ienapplicant.entity';
import { ApplicantActiveFlagRO } from '@ien/common';
import { IENApplicantStatus } from './ienapplicant-status.entity';

@Entity('ien_applicants_active_flag')
export class IENApplicantActiveFlag {
Expand All @@ -13,15 +14,23 @@ export class IENApplicantActiveFlag {
@Column({ default: true })
is_active!: boolean;

@Column({ type: 'uuid', nullable: true })
status_id?: string;

@ManyToOne(() => IENApplicant, applicant => applicant.id)
@JoinColumn({ name: 'applicant_id' })
applicant!: IENApplicant;

@ManyToOne(() => IENApplicantStatus, status => status.id)
@JoinColumn({ name: 'status_id' })
status?: IENApplicantStatus;

toResponseObject(): ApplicantActiveFlagRO {
return {
applicant_id: this.applicant_id,
ha_id: this.ha_id,
is_active: this.is_active,
status: this.status?.status,
};
}
}
39 changes: 39 additions & 0 deletions apps/api/src/endofjourney.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NestFactory } from '@nestjs/core';
import { Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';
import { AppLogger } from './common/logger.service';
import { EndOfJourneyService } from './applicant/endofjourney.service';

/**
* Design this function to trigger existing NestJs application services without Api-Gateway
* All the schedule and background job trigger will be added here.
* Operation like sync data, update database view or trigger db function, etc.
*/
export const handler: Handler = async (event, context: Context) => {
const app = await NestFactory.createApplicationContext(AppModule);
const eojService = app.get(EndOfJourneyService);
const logger = app.get(AppLogger);

logger.log(event, 'END-OF-JOURNEY');
logger.log(context, 'END-OF-JOURNEY');

try {
switch (event.path) {
case 'end-of-journey-complete':
logger.log('Start end of journey complete check...', 'END-OF-JOURNEY');
await eojService.init();
break;
}
} catch (e) {
logger.error(e, 'END-OF-JOURNEY');
}
logger.log('...end end of journey complete check', 'END-OF-JOURNEY');
await app.close();
};

/**
* To be locally run by Yarn
*/
if (require.main === module) {
handler({ path: `${process.argv.pop()}-data` }, {} as Context, () => void 0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddEndOfJourneyApplicantStatus1729889576502 implements MigrationInterface {
milestones = [
{
id: '706d97bf-7e5a-4d3a-b283-fb3a9858c2ff',
status: 'End of Journey - Journey Complete',
category: 'IEN Licensing/Registration Process',
},
{
id: '9fedd3db-a992-4672-afc0-fc27253170e1',
status: 'End of Journey - Journey Incomplete',
category: 'IEN Licensing/Registration Process',
},
];

private async addMilestones(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager
.createQueryBuilder()
.insert()
.into('ien_applicant_status')
.values(this.milestones)
.orIgnore(true)
.execute();
}

public async up(queryRunner: QueryRunner): Promise<void> {
await this.addMilestones(queryRunner);
}

public async down(queryRunner: QueryRunner): Promise<void> {
const ids = this.milestones.map(milestone => milestone.id);

await queryRunner.manager
.createQueryBuilder()
.delete()
.from('ien_applicant_status')
.whereInIds(ids)
.execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm';

export class AddStatusIdToIenApplicantsActiveFlag1729890423444 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add the new column 'status_id' to 'ien_applicants_active_flag' table
await queryRunner.addColumn(
'ien_applicants_active_flag',
new TableColumn({
name: 'status_id',
type: 'uuid',
isNullable: true, // Allow null values to support ON DELETE SET NULL
}),
);

// Add foreign key constraint to 'status_id'
await queryRunner.createForeignKey(
'ien_applicants_active_flag',
new TableForeignKey({
columnNames: ['status_id'],
referencedTableName: 'ien_applicant_status',
referencedColumnNames: ['id'],
onDelete: 'SET NULL', // Set to null if the referenced row is deleted
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Drop the foreign key constraint first
const table = await queryRunner.getTable('ien_applicants_active_flag');
if (!table) {
return;
}

const foreignKey = table.foreignKeys.find(fk => fk.columnNames.indexOf('status_id') !== -1);
if (!foreignKey) {
return;
}
await queryRunner.dropForeignKey('ien_applicants_active_flag', foreignKey);

// Then drop the 'status_id' column
await queryRunner.dropColumn('ien_applicants_active_flag', 'status_id');
}
}
4 changes: 4 additions & 0 deletions apps/api/src/report/types/milestone-table-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,8 @@ export type MilestoneDurationTableEntry<T> = {
[STATUS.BCCNM_DECISION_DATE]?: T;

[STATUS.BCCNM_REGISTRATION_DATE]?: T;

[STATUS.END_OF_JOURNEY_COMPLETE]?: T;

[STATUS.END_OF_JOURNEY_INCOMPLETE]?: T;
};
4 changes: 4 additions & 0 deletions packages/common/src/enum/milestone-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export enum STATUS {
BCCNM_APPLICATION_COMPLETE_DATE = 'BCCNM Application Complete Date',
BCCNM_DECISION_DATE = 'BCCNM Decision Date',
BCCNM_REGISTRATION_DATE = 'BCCNM Registration Date',

// End of Journey
END_OF_JOURNEY_COMPLETE = 'End of Journey - Journey Complete',
END_OF_JOURNEY_INCOMPLETE = 'End of Journey - Journey Incomplete',
}

export const COMPLETED_STATUSES = [
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/ro/applicant.ro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface ApplicantActiveFlagRO {
applicant_id: string;
ha_id: string;
is_active: boolean;
status?: string;
}

export interface PathwayRO {
Expand Down
Loading

0 comments on commit 4186498

Please sign in to comment.