diff --git a/src/db/migrations/1757915357218-add-deleted-at-column-to-junior.ts b/src/db/migrations/1757915357218-add-deleted-at-column-to-junior.ts new file mode 100644 index 0000000..452d466 --- /dev/null +++ b/src/db/migrations/1757915357218-add-deleted-at-column-to-junior.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDeletedAtColumnToJunior1757915357218 implements MigrationInterface { + name = 'AddDeletedAtColumnToJunior1757915357218'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "juniors" ADD "deleted_at" TIMESTAMP WITH TIME ZONE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "juniors" DROP COLUMN "deleted_at"`); + } +} diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index dbdede5..ca2c3b5 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -3,3 +3,4 @@ export * from './1754915164809-create-neoleap-related-entities'; export * from './1754915164810-seed-default-avatar'; export * from './1757349525708-create-money-requests-table'; export * from './1757433339849-add-reservation-amount-to-account-entity'; +export * from './1757915357218-add-deleted-at-column-to-junior'; diff --git a/src/i18n/ar/app.json b/src/i18n/ar/app.json index 24735cb..4b63817 100644 --- a/src/i18n/ar/app.json +++ b/src/i18n/ar/app.json @@ -68,7 +68,8 @@ "CIVIL_ID_REQUIRED": "مطلوب بطاقة الهوية المدنية.", "CIVIL_ID_NOT_CREATED_BY_GUARDIAN": "تم تحميل بطاقة الهوية المدنية من قبل شخص آخر غير ولي الأمر.", "CIVIL_ID_ALREADY_EXISTS": "بطاقة الهوية المدنية مستخدمة بالفعل من قبل طفل آخر.", - "CANNOT_UPDATE_REGISTERED_USER": "الطفل قد سجل بالفعل. لا يُسمح بتحديث البيانات." + "CANNOT_UPDATE_REGISTERED_USER": "الطفل قد سجل بالفعل. لا يُسمح بتحديث البيانات.", + "CANNOT_DELETE_REGISTERED_USER": "الطفل قد سجل بالفعل. لا يُسمح بحذف الطفل." }, "MONEY_REQUEST": { diff --git a/src/i18n/en/app.json b/src/i18n/en/app.json index 48f2bd7..f661688 100644 --- a/src/i18n/en/app.json +++ b/src/i18n/en/app.json @@ -67,7 +67,8 @@ "CIVIL_ID_REQUIRED": "Civil ID is required.", "CIVIL_ID_NOT_CREATED_BY_GUARDIAN": "The civil ID document was not uploaded by the guardian.", "CIVIL_ID_ALREADY_EXISTS": "The civil ID is already used by another junior.", - "CANNOT_UPDATE_REGISTERED_USER": "The junior has already registered. Updating details is not allowed." + "CANNOT_UPDATE_REGISTERED_USER": "The junior has already registered. Updating details is not allowed.", + "CANNOT_DELETE_REGISTERED_USER": "The junior has already registered. Deleting the junior is not allowed." }, "MONEY_REQUEST": { diff --git a/src/junior/controllers/junior.controller.ts b/src/junior/controllers/junior.controller.ts index 230c4e3..81f2118 100644 --- a/src/junior/controllers/junior.controller.ts +++ b/src/junior/controllers/junior.controller.ts @@ -1,4 +1,16 @@ -import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Patch, + Post, + Query, + UseGuards, +} from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { Roles } from '~/auth/enums'; import { IJwtPayload } from '~/auth/interfaces'; @@ -83,6 +95,14 @@ export class JuniorController { return ResponseFactory.data(new JuniorResponseDto(junior)); } + @Delete(':juniorId') + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) + @HttpCode(HttpStatus.NO_CONTENT) + async deleteJunior(@AuthenticatedUser() user: IJwtPayload, @Param('juniorId', CustomParseUUIDPipe) juniorId: string) { + await this.juniorService.deleteJunior(juniorId, user.sub); + } + @Post('set-theme') @UseGuards(RolesGuard) @AllowedRoles(Roles.JUNIOR) diff --git a/src/junior/entities/junior.entity.ts b/src/junior/entities/junior.entity.ts index 47891a2..2349f56 100644 --- a/src/junior/entities/junior.entity.ts +++ b/src/junior/entities/junior.entity.ts @@ -2,6 +2,7 @@ import { BaseEntity, Column, CreateDateColumn, + DeleteDateColumn, Entity, JoinColumn, ManyToOne, @@ -49,4 +50,7 @@ export class Junior extends BaseEntity { @UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' }) updatedAt!: Date; + + @DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true }) + deletedAt!: Date | null; } diff --git a/src/junior/repositories/junior.repository.ts b/src/junior/repositories/junior.repository.ts index 4fea55e..22cd1e0 100644 --- a/src/junior/repositories/junior.repository.ts +++ b/src/junior/repositories/junior.repository.ts @@ -65,4 +65,8 @@ export class JuniorRepository { }), ); } + + softDelete(juniorId: string) { + return this.juniorRepository.softDelete({ id: juniorId }); + } } diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts index acb611e..b864746 100644 --- a/src/junior/services/junior.service.ts +++ b/src/junior/services/junior.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { IsNull, Not } from 'typeorm'; import { Transactional } from 'typeorm-transactional'; import { Roles } from '~/auth/enums'; import { CardService } from '~/card/services'; @@ -183,6 +184,31 @@ export class JuniorService { return this.cardService.transferToChild(juniorId, body.amount); } + async deleteJunior(juniorId: string, guardianId: string) { + const doesBelong = await this.doesJuniorBelongToGuardian(guardianId, juniorId); + + if (!doesBelong) { + this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`); + throw new BadRequestException('JUNIOR.NOT_BELONG_TO_GUARDIAN'); + } + + const hasPassword = await this.userService.findUser({ id: juniorId, password: Not(IsNull()) }); + + if (hasPassword) { + this.logger.error(`Cannot delete junior ${juniorId} with registered user`); + throw new BadRequestException('JUNIOR.CANNOT_DELETE_REGISTERED_USER'); + } + + const { affected } = await this.juniorRepository.softDelete(juniorId); + + if (affected === 0) { + this.logger.error(`Junior ${juniorId} not found`); + throw new BadRequestException('JUNIOR.NOT_FOUND'); + } + + this.logger.log(`Junior ${juniorId} deleted successfully`); + } + private async prepareJuniorImages(juniors: Junior[]) { this.logger.log(`Preparing junior images`); await Promise.all(