Skip to content

Commit

Permalink
Merge pull request #1946 from bcgov/feature/ALCS-2345-tags-on-nois-ba…
Browse files Browse the repository at this point in the history
…ckend

ALCS-2345 Backend implementation
  • Loading branch information
fbarreta authored Oct 31, 2024
2 parents 01c2503 + afde00e commit 718a10e
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 20 deletions.
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"`);
}

}

0 comments on commit 718a10e

Please sign in to comment.