Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ALCS-2345 Backend implementation #1946

Merged
merged 4 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NoticeOfIntentTagController } from './notice-of-intent-tag.controller';
import { NoticeOfIntentTagService } from './notice-of-intent-tag.service';
import { DeepMocked } from '@golevelup/nestjs-testing';
import { ClsService } from 'nestjs-cls';
import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes';

describe('NoticeOfIntentTagController', () => {
let controller: NoticeOfIntentTagController;
let tagService: DeepMocked<NoticeOfIntentTagService>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NoticeOfIntentTagController],
providers: [
{ provide: NoticeOfIntentTagService, useValue: tagService },
{
provide: ClsService,
useValue: {},
},
...mockKeyCloakProviders,
],
}).compile();

controller = module.get<NoticeOfIntentTagController>(NoticeOfIntentTagController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiOAuth2 } from '@nestjs/swagger';
import { RolesGuard } from '../../../common/authorization/roles-guard.service';
import * as config from 'config';
import { UserRoles } from '../../../common/authorization/roles.decorator';
import { ROLES_ALLOWED_APPLICATIONS } from '../../../common/authorization/roles';
import { NoticeOfIntentTagService } from './notice-of-intent-tag.service';
import { NoticeOfIntentTagDto } from './notice-of-intent-tag.dto';

@Controller('notice-of-intent/:fileNumber/tag')
@ApiOAuth2(config.get<string[]>('KEYCLOAK.SCOPES'))
@UseGuards(RolesGuard)
export class NoticeOfIntentTagController {
constructor(private service: NoticeOfIntentTagService) {}

@Get('')
@UserRoles(...ROLES_ALLOWED_APPLICATIONS)
async getApplicationTags(@Param('fileNumber') fileNumber: string) {
return await this.service.getNoticeOfIntentTags(fileNumber);
}

@Post('')
@UserRoles(...ROLES_ALLOWED_APPLICATIONS)
async addTagToApplication(@Param('fileNumber') fileNumber: string, @Body() dto: NoticeOfIntentTagDto) {
return await this.service.addTagToNoticeOfIntent(fileNumber, dto.tagName);
}

@Delete('/:tagName')
@UserRoles(...ROLES_ALLOWED_APPLICATIONS)
async removeTagFromApplication(@Param('fileNumber') fileNumber: string, @Param('tagName') tagName: string) {
return await this.service.removeTagFromNoticeOfIntent(fileNumber, tagName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';

export class NoticeOfIntentTagDto {
@IsString()
tagName: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NoticeOfIntentTagService } from './notice-of-intent-tag.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NoticeOfIntent } from '../notice-of-intent.entity';
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
import { Repository } from 'typeorm';
import { Tag } from '../../tag/tag.entity';

describe('NoticeOfIntentTagService', () => {
let service: NoticeOfIntentTagService;
let noiRepository: DeepMocked<Repository<NoticeOfIntent>>;
let tagRepository: DeepMocked<Repository<Tag>>;

beforeEach(async () => {
noiRepository = createMock();
const module: TestingModule = await Test.createTestingModule({
providers: [
NoticeOfIntentTagService,
{
provide: getRepositoryToken(NoticeOfIntent),
useValue: noiRepository,
},
{
provide: getRepositoryToken(Tag),
useValue: tagRepository,
},
],
}).compile();

service = module.get<NoticeOfIntentTagService>(NoticeOfIntentTagService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Tag } from '../../tag/tag.entity';
import { NoticeOfIntent } from '../notice-of-intent.entity';
import { Repository } from 'typeorm';
import { ServiceNotFoundException, ServiceValidationException } from '@app/common/exceptions/base.exception';

@Injectable()
export class NoticeOfIntentTagService {
constructor(
@InjectRepository(Tag) private tagRepository: Repository<Tag>,
@InjectRepository(NoticeOfIntent) private noiRepository: Repository<NoticeOfIntent>,
) {}

async addTagToNoticeOfIntent(fileNumber: string, tagName: string) {
const noi = await this.noiRepository.findOne({
where: { fileNumber: fileNumber },
relations: ['tags'],
});
if (!noi) {
throw new ServiceNotFoundException(`Notice of Intent not found with number ${fileNumber}`);
}

const tag = await this.tagRepository.findOne({ where: { name: tagName } });
if (!tag) {
throw new ServiceNotFoundException(`Tag not found with name ${tagName}`);
}

if (!noi.tags) {
noi.tags = [];
}

const tagExists = noi.tags.some((t) => t.uuid === tag.uuid);
console.log(tagExists);
if (tagExists) {
throw new ServiceValidationException(`Tag ${tagName} already exists`);
}

noi.tags.push(tag);
return this.noiRepository.save(noi);
}

async removeTagFromNoticeOfIntent(fileNumber: string, tagName: string) {
const noi = await this.noiRepository.findOne({
where: { fileNumber: fileNumber },
relations: ['tags'],
});
if (!noi) {
throw new ServiceNotFoundException(`Notice of Intent not found with number ${fileNumber}`);
}

const tag = await this.tagRepository.findOne({ where: { name: tagName } });
if (!tag) {
throw new ServiceNotFoundException(`Tag not found with name ${tagName}`);
}

if (!noi.tags) {
noi.tags = [];
}

const tagExists = noi.tags.some((t) => t.uuid === tag.uuid);
if (!tagExists) {
throw new ServiceValidationException(`Tag ${tagName} does not exist`);
}

noi.tags = noi.tags.filter((t) => t.uuid !== tag.uuid);
return this.noiRepository.save(noi);
}

async getNoticeOfIntentTags(fileNumber: string) {
const noi = await this.noiRepository.findOne({
where: { fileNumber: fileNumber },
relations: ['tags'],
order: { auditCreatedAt: 'ASC' },
});
if (!noi) {
throw new ServiceNotFoundException(`Notice of Intent not found with number ${fileNumber}`);
}
return noi.tags && noi.tags.length > 0 ? noi.tags : [];
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { AutoMap } from 'automapper-classes';
import { Type } from 'class-transformer';
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
OneToOne,
} from 'typeorm';
import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, OneToOne } from 'typeorm';
import { Base } from '../../common/entities/base.entity';
import { ColumnNumericTransformer } from '../../utils/column-numeric-transform';
import { Card } from '../card/card.entity';
Expand All @@ -19,10 +9,10 @@ import { LocalGovernment } from '../local-government/local-government.entity';
import { NoticeOfIntentDocument } from './notice-of-intent-document/notice-of-intent-document.entity';
import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity';
import { NoticeOfIntentType } from './notice-of-intent-type/notice-of-intent-type.entity';
import { Tag } from '../tag/tag.entity';

@Entity({
comment:
'Base data for Notice of Intents incl. the ID, key dates, and the date of the first decision',
comment: 'Base data for Notice of Intents incl. the ID, key dates, and the date of the first decision',
})
export class NoticeOfIntent extends Base {
constructor(data?: Partial<NoticeOfIntent>) {
Expand Down Expand Up @@ -171,8 +161,7 @@ export class NoticeOfIntent extends Base {
@AutoMap(() => String)
@Column({
type: 'text',
comment:
'NOI Id that is applicable only to paper version applications from 70s - 80s',
comment: 'NOI Id that is applicable only to paper version applications from 70s - 80s',
nullable: true,
})
legacyId?: string | null;
Expand Down Expand Up @@ -247,9 +236,11 @@ export class NoticeOfIntent extends Base {
typeCode: string;

@AutoMap()
@OneToMany(
() => NoticeOfIntentDocument,
(noiDocument) => noiDocument.noticeOfIntent,
)
@OneToMany(() => NoticeOfIntentDocument, (noiDocument) => noiDocument.noticeOfIntent)
documents: NoticeOfIntentDocument[];

@AutoMap(() => [Tag])
@ManyToMany(() => Tag, (tag) => tag.noticeOfIntents)
@JoinTable({ name: 'notice_of_intent_tag' })
tags: Tag[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { NoticeOfIntentType } from './notice-of-intent-type/notice-of-intent-typ
import { NoticeOfIntentController } from './notice-of-intent.controller';
import { NoticeOfIntent } from './notice-of-intent.entity';
import { NoticeOfIntentService } from './notice-of-intent.service';
import { TagModule } from '../tag/tag.module';
import { NoticeOfIntentTagService } from './notice-of-intent-tag/notice-of-intent-tag.service';
import { NoticeOfIntentTagController } from './notice-of-intent-tag/notice-of-intent-tag.controller';

@Module({
imports: [
Expand All @@ -52,6 +55,7 @@ import { NoticeOfIntentService } from './notice-of-intent.service';
LocalGovernmentModule,
NoticeOfIntentSubmissionStatusModule,
forwardRef(() => NoticeOfIntentSubmissionModule),
TagModule,
],
providers: [
NoticeOfIntentService,
Expand All @@ -60,19 +64,22 @@ import { NoticeOfIntentService } from './notice-of-intent.service';
NoticeOfIntentDocumentService,
NoticeOfIntentSubmissionService,
NoticeOfIntentParcelProfile,
NoticeOfIntentTagService,
],
controllers: [
NoticeOfIntentController,
NoticeOfIntentMeetingController,
NoticeOfIntentDocumentController,
NoticeOfIntentSubmissionController,
NoticeOfIntentParcelController,
NoticeOfIntentTagController,
],
exports: [
NoticeOfIntentService,
NoticeOfIntentMeetingService,
NoticeOfIntentDocumentService,
NoticeOfIntentSubmissionService,
NoticeOfIntentTagService,
],
})
export class NoticeOfIntentModule {}
6 changes: 5 additions & 1 deletion services/apps/alcs/src/alcs/tag/tag.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AutoMap } from 'automapper-classes';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Base } from '../../common/entities/base.entity';
import { TagCategory } from './tag-category/tag-category.entity';
import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity';

@Entity({ comment: 'Tag.' })
export class Tag extends Base {
Expand All @@ -28,4 +29,7 @@ export class Tag extends Base {
nullable: true,
})
category?: TagCategory | null;

@ManyToMany(() => NoticeOfIntent, (noticeOfIntent) => noticeOfIntent.tags)
noticeOfIntents: NoticeOfIntent[];
}
1 change: 1 addition & 0 deletions services/apps/alcs/src/alcs/tag/tag.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ import { TagService } from './tag.service';
imports: [TypeOrmModule.forFeature([TagCategory, Tag])],
controllers: [TagCategoryController, TagController],
providers: [TagCategoryService, TagService],
exports: [TypeOrmModule],
})
export class TagModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddTagsToNoi1730326975258 implements MigrationInterface {
name = 'AddTagsToNoi1730326975258'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "alcs"."notice_of_intent_tag" ("notice_of_intent_uuid" uuid NOT NULL, "tag_uuid" uuid NOT NULL, CONSTRAINT "PK_8ae82272ffcbd27427172fd5e11" PRIMARY KEY ("notice_of_intent_uuid", "tag_uuid"))`);
await queryRunner.query(`CREATE INDEX "IDX_2baab887c8e66032ba78750b91" ON "alcs"."notice_of_intent_tag" ("notice_of_intent_uuid") `);
await queryRunner.query(`CREATE INDEX "IDX_404540b8fc70a267572f0d506a" ON "alcs"."notice_of_intent_tag" ("tag_uuid") `);
await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_tag" ADD CONSTRAINT "FK_2baab887c8e66032ba78750b912" FOREIGN KEY ("notice_of_intent_uuid") REFERENCES "alcs"."notice_of_intent"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_tag" ADD CONSTRAINT "FK_404540b8fc70a267572f0d506aa" FOREIGN KEY ("tag_uuid") REFERENCES "alcs"."tag"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_tag" DROP CONSTRAINT "FK_404540b8fc70a267572f0d506aa"`);
await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_tag" DROP CONSTRAINT "FK_2baab887c8e66032ba78750b912"`);
await queryRunner.query(`DROP INDEX "alcs"."IDX_404540b8fc70a267572f0d506a"`);
await queryRunner.query(`DROP INDEX "alcs"."IDX_2baab887c8e66032ba78750b91"`);
await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_tag"`);
}

}