From 970a41c8957df4f62d2c4078cd130ee776f051fc Mon Sep 17 00:00:00 2001 From: Abdalhamid Alhamad Date: Mon, 9 Dec 2024 13:11:18 +0300 Subject: [PATCH] feat: create junior --- package-lock.json | 88 ++++++++++++++++++- package.json | 5 +- src/app.module.ts | 21 ++++- src/auth/auth.module.ts | 9 +- src/auth/repositories/user.repository.ts | 18 ++-- src/auth/services/user.service.ts | 29 ++++-- src/customer/customer.module.ts | 5 +- src/customer/entities/customer.entity.ts | 8 ++ .../repositories/customer.repository.ts | 14 +++ src/customer/services/customer.service.ts | 12 ++- .../1733731507261-create-junior-entity.ts | 37 ++++++++ .../1733732021622-create-guardian-entity.ts | 30 +++++++ src/db/migrations/index.ts | 2 + src/document/entities/document.entity.ts | 7 ++ src/dtos/request/index.ts | 0 src/guardian/entities/guradian.entity.ts | 35 ++++++++ src/guardian/guardian.module.ts | 8 ++ src/junior/controllers/index.ts | 1 + src/junior/controllers/junior.controller.ts | 56 ++++++++++++ .../request/create-junior-user.request.dto.ts | 37 ++++++++ .../dtos/request/create-junior.request.dto.ts | 18 ++++ src/junior/dtos/request/index.ts | 2 + src/junior/dtos/response/index.ts | 1 + .../dtos/response/junior.response.dto.ts | 24 +++++ src/junior/entities/index.ts | 1 + src/junior/entities/junior.entity.ts | 58 ++++++++++++ src/junior/enums/index.ts | 1 + src/junior/enums/relationship.enum.ts | 4 + src/junior/junior.module.ts | 15 ++++ src/junior/repositories/index.ts | 1 + src/junior/repositories/junior.repository.ts | 26 ++++++ src/junior/services/index.ts | 1 + src/junior/services/junior.service.ts | 65 ++++++++++++++ src/main.ts | 8 ++ typeorm.cli.ts | 4 +- 35 files changed, 625 insertions(+), 26 deletions(-) create mode 100644 src/db/migrations/1733731507261-create-junior-entity.ts create mode 100644 src/db/migrations/1733732021622-create-guardian-entity.ts delete mode 100644 src/dtos/request/index.ts create mode 100644 src/guardian/entities/guradian.entity.ts create mode 100644 src/guardian/guardian.module.ts create mode 100644 src/junior/controllers/index.ts create mode 100644 src/junior/controllers/junior.controller.ts create mode 100644 src/junior/dtos/request/create-junior-user.request.dto.ts create mode 100644 src/junior/dtos/request/create-junior.request.dto.ts create mode 100644 src/junior/dtos/request/index.ts create mode 100644 src/junior/dtos/response/index.ts create mode 100644 src/junior/dtos/response/junior.response.dto.ts create mode 100644 src/junior/entities/index.ts create mode 100644 src/junior/entities/junior.entity.ts create mode 100644 src/junior/enums/index.ts create mode 100644 src/junior/enums/relationship.enum.ts create mode 100644 src/junior/junior.module.ts create mode 100644 src/junior/repositories/index.ts create mode 100644 src/junior/repositories/junior.repository.ts create mode 100644 src/junior/services/index.ts create mode 100644 src/junior/services/junior.service.ts diff --git a/package-lock.json b/package-lock.json index d4a770c..7890633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,9 +45,10 @@ "pg": "^8.13.1", "pino-http": "^10.3.0", "pino-pretty": "^13.0.0", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "typeorm-transactional": "^0.5.0" }, "devDependencies": { "@golevelup/ts-jest": "^0.6.0", @@ -2743,6 +2744,15 @@ "@types/node": "*" } }, + "node_modules/@types/cls-hooked": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@types/cls-hooked/-/cls-hooked-4.3.9.tgz", + "integrity": "sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -3966,6 +3976,18 @@ "devOptional": true, "license": "MIT" }, + "node_modules/async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "license": "MIT", + "dependencies": { + "stack-chain": "^1.3.7" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5011,6 +5033,29 @@ "node": ">=0.8" } }, + "node_modules/cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "license": "BSD-2-Clause", + "dependencies": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" + } + }, + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -5796,6 +5841,15 @@ "dev": true, "license": "ISC" }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "license": "BSD-2-Clause", + "dependencies": { + "shimmer": "^1.2.0" + } + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -14467,6 +14521,12 @@ "node": ">=8" } }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -14648,6 +14708,12 @@ "node": ">=0.10.0" } }, + "node_modules/stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==", + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -15601,6 +15667,24 @@ } } }, + "node_modules/typeorm-transactional": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/typeorm-transactional/-/typeorm-transactional-0.5.0.tgz", + "integrity": "sha512-53/CwnXpOIJnWU3oVCNbhHB95FwciKSGbY+m/Hw4e2dBM2c4toiOHwf4pmk83Ne7guznmDgVr/5IUfbp+JTPCg==", + "license": "MIT", + "dependencies": { + "@types/cls-hooked": "^4.3.3", + "cls-hooked": "^4.2.2", + "semver": "^7.5.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "reflect-metadata": ">= 0.1.12", + "typeorm": ">= 0.2.8" + } + }, "node_modules/typeorm/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", diff --git a/package.json b/package.json index 4d7ea04..e41907c 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,10 @@ "pg": "^8.13.1", "pino-http": "^10.3.0", "pino-pretty": "^13.0.0", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "typeorm-transactional": "^0.5.0" }, "devDependencies": { "@golevelup/ts-jest": "^0.6.0", diff --git a/src/app.module.ts b/src/app.module.ts index 6f215f2..ebee151 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,8 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; import { I18nMiddleware, I18nModule } from 'nestjs-i18n'; import { LoggerModule } from 'nestjs-pino'; +import { DataSource } from 'typeorm'; +import { addTransactionalDataSource } from 'typeorm-transactional'; import { AuthModule } from './auth/auth.module'; import { OtpModule } from './common/modules/otp/otp.module'; import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters'; @@ -13,7 +15,9 @@ import { buildValidationPipe } from './core/pipes'; import { CustomerModule } from './customer/customer.module'; import { migrations } from './db'; import { DocumentModule } from './document/document.module'; +import { GuardianModule } from './guardian/guardian.module'; import { HealthModule } from './health/health.module'; +import { JuniorModule } from './junior/junior.module'; @Module({ controllers: [], imports: [ @@ -21,7 +25,18 @@ import { HealthModule } from './health/health.module'; TypeOrmModule.forRootAsync({ imports: [], inject: [ConfigService], - useFactory: (config: ConfigService) => buildTypeormOptions(config, migrations), + useFactory: (config: ConfigService) => { + return buildTypeormOptions(config, migrations); + }, + /* eslint-disable require-await */ + async dataSourceFactory(options) { + if (!options) { + throw new Error('Invalid options passed'); + } + + return addTransactionalDataSource(new DataSource(options)); + }, + /* eslint-enable require-await */ }), LoggerModule.forRootAsync({ useFactory: (config: ConfigService) => buildLoggerOptions(config), @@ -31,9 +46,11 @@ import { HealthModule } from './health/health.module'; // App modules AuthModule, CustomerModule, + JuniorModule, + GuardianModule, + OtpModule, DocumentModule, HealthModule, - OtpModule, ], providers: [ // Global Pipes diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index be7076c..4dfb072 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { CustomerModule } from '~/customer/customer.module'; import { AuthController } from './controllers'; import { Device, User, UserNotificationSettings } from './entities'; import { DeviceRepository, UserRepository } from './repositories'; @@ -9,7 +10,11 @@ import { UserService } from './services/user.service'; import { AccessTokenStrategy } from './strategies'; @Module({ - imports: [TypeOrmModule.forFeature([User, UserNotificationSettings, Device]), JwtModule.register({})], + imports: [ + TypeOrmModule.forFeature([User, UserNotificationSettings, Device]), + JwtModule.register({}), + forwardRef(() => CustomerModule), + ], providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy], controllers: [AuthController], exports: [UserService], diff --git a/src/auth/repositories/user.repository.ts b/src/auth/repositories/user.repository.ts index dedda16..a01e5ec 100644 --- a/src/auth/repositories/user.repository.ts +++ b/src/auth/repositories/user.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere, Repository } from 'typeorm'; import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request'; -import { Customer } from '~/customer/entities'; import { User, UserNotificationSettings } from '../entities'; @Injectable() @@ -20,7 +19,7 @@ export class UserRepository { ); } - findOne(where: FindOptionsWhere) { + findOne(where: FindOptionsWhere | FindOptionsWhere[]) { return this.userRepository.findOne({ where }); } @@ -29,13 +28,16 @@ export class UserRepository { return this.userRepository.save(user); } - verifyUserAndCreateCustomer(user: User) { - user.customer = Customer.create({ ...user.customer, id: user.id, isGuardian: true }); - - return this.userRepository.save(user); - } - update(userId: string, data: Partial) { return this.userRepository.update(userId, data); } + + createUser(data: Partial) { + const user = this.userRepository.create({ + ...data, + notificationSettings: UserNotificationSettings.create(), + }); + + return this.userRepository.save(user); + } } diff --git a/src/auth/services/user.service.ts b/src/auth/services/user.service.ts index c41fc03..47579e2 100644 --- a/src/auth/services/user.service.ts +++ b/src/auth/services/user.service.ts @@ -1,6 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request'; +import { CustomerService } from '~/customer/services'; +import { Guardian } from '~/guardian/entities/guradian.entity'; import { CreateUnverifiedUserRequestDto } from '../dtos/request'; import { User } from '../entities'; import { Roles } from '../enums'; @@ -8,7 +10,10 @@ import { UserRepository } from '../repositories'; @Injectable() export class UserService { - constructor(private readonly userRepository: UserRepository) {} + constructor( + private readonly userRepository: UserRepository, + @Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService, + ) {} async updateNotificationSettings(userId: string, body: UpdateNotificationsSettingsRequestDto) { const user = await this.findUserOrThrow({ id: userId }); @@ -19,7 +24,7 @@ export class UserService { return notificationSettings; } - findUser(where: FindOptionsWhere) { + findUser(where: FindOptionsWhere | FindOptionsWhere[]) { return this.userRepository.findOne(where); } @@ -44,12 +49,19 @@ export class UserService { } if (user && user.roles.includes(Roles.JUNIOR)) { + throw new BadRequestException('USERS.JUNIOR_UPGRADE_NOT_SUPPORTED_YET'); //TODO add role Guardian to the existing user and send OTP } return user; } + async createUser(data: Partial) { + const user = await this.userRepository.createUser(data); + + return user; + } + setEmail(userId: string, email: string) { return this.userRepository.update(userId, { email }); } @@ -58,7 +70,14 @@ export class UserService { return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true }); } - verifyUserAndCreateCustomer(user: User) { - return this.userRepository.verifyUserAndCreateCustomer(user); + async verifyUserAndCreateCustomer(user: User) { + await this.customerService.createCustomer( + { + guardian: Guardian.create({ id: user.id }), + }, + user, + ); + + return this.findUserOrThrow({ id: user.id }); } } diff --git a/src/customer/customer.module.ts b/src/customer/customer.module.ts index da7d8a8..7ce58d0 100644 --- a/src/customer/customer.module.ts +++ b/src/customer/customer.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthModule } from '~/auth/auth.module'; import { CustomerController } from './controllers'; @@ -7,8 +7,9 @@ import { CustomerRepository } from './repositories/customer.repository'; import { CustomerService } from './services'; @Module({ - imports: [TypeOrmModule.forFeature([Customer]), AuthModule], + imports: [TypeOrmModule.forFeature([Customer]), forwardRef(() => AuthModule)], controllers: [CustomerController], providers: [CustomerService, CustomerRepository], + exports: [CustomerService], }) export class CustomerModule {} diff --git a/src/customer/entities/customer.entity.ts b/src/customer/entities/customer.entity.ts index 6c6b88d..8cd62ad 100644 --- a/src/customer/entities/customer.entity.ts +++ b/src/customer/entities/customer.entity.ts @@ -9,6 +9,8 @@ import { UpdateDateColumn, } from 'typeorm'; import { User } from '~/auth/entities'; +import { Guardian } from '~/guardian/entities/guradian.entity'; +import { Junior } from '~/junior/entities'; @Entity() export class Customer extends BaseEntity { @@ -67,6 +69,12 @@ export class Customer extends BaseEntity { @JoinColumn({ name: 'user_id' }) user!: User; + @OneToOne(() => Junior, (junior) => junior.customer, { cascade: true }) + junior!: Junior; + + @OneToOne(() => Guardian, (guardian) => guardian.customer, { cascade: true }) + guardian!: Guardian; + @CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' }) createdAt!: Date; diff --git a/src/customer/repositories/customer.repository.ts b/src/customer/repositories/customer.repository.ts index f0ddb5d..4bac158 100644 --- a/src/customer/repositories/customer.repository.ts +++ b/src/customer/repositories/customer.repository.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere, Repository } from 'typeorm'; +import { User } from '~/auth/entities'; +import { Roles } from '~/auth/enums'; import { UpdateCustomerRequestDto } from '../dtos/request'; import { Customer } from '../entities'; @@ -15,4 +17,16 @@ export class CustomerRepository { findOne(where: FindOptionsWhere) { return this.customerRepository.findOne({ where }); } + + createCustomer(customerData: Partial, user: User) { + return this.customerRepository.save( + this.customerRepository.create({ + ...customerData, + id: user.id, + user, + isGuardian: user.roles.includes(Roles.GUARDIAN), + isJunior: user.roles.includes(Roles.JUNIOR), + }), + ); + } } diff --git a/src/customer/services/customer.service.ts b/src/customer/services/customer.service.ts index f732814..f7ca3ff 100644 --- a/src/customer/services/customer.service.ts +++ b/src/customer/services/customer.service.ts @@ -1,4 +1,5 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common'; +import { User } from '~/auth/entities'; import { UserService } from '~/auth/services/user.service'; import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request'; import { Customer } from '../entities'; @@ -6,7 +7,10 @@ import { CustomerRepository } from '../repositories/customer.repository'; @Injectable() export class CustomerService { - constructor(private readonly userService: UserService, private readonly customerRepository: CustomerRepository) {} + constructor( + @Inject(forwardRef(() => UserService)) private readonly userService: UserService, + private readonly customerRepository: CustomerRepository, + ) {} updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) { return this.userService.updateNotificationSettings(userId, data); } @@ -16,6 +20,10 @@ export class CustomerService { return this.findCustomerById(userId); } + createCustomer(customerData: Partial, user: User) { + return this.customerRepository.createCustomer(customerData, user); + } + async findCustomerById(id: string) { const customer = await this.customerRepository.findOne({ id }); if (!customer) { diff --git a/src/db/migrations/1733731507261-create-junior-entity.ts b/src/db/migrations/1733731507261-create-junior-entity.ts new file mode 100644 index 0000000..4467c60 --- /dev/null +++ b/src/db/migrations/1733731507261-create-junior-entity.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateJuniorEntity1733731507261 implements MigrationInterface { + name = 'CreateJuniorEntity1733731507261'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "juniors" + ("id" uuid NOT NULL, + "relationship" character varying(255) NOT NULL, + "civil_id_front_id" uuid NOT NULL, + "civil_id_back_id" uuid NOT NULL, + "customer_id" uuid NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "REL_6a72e1a5758643737cc563b96c" UNIQUE ("civil_id_front_id"), + CONSTRAINT "REL_4662c4433223c01fe69fc1382f" UNIQUE ("civil_id_back_id"), + CONSTRAINT "REL_dfbf64ede1ff823a489902448a" UNIQUE ("customer_id"), CONSTRAINT "PK_2d273092322c1f8bf26296fa608" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "juniors" ADD CONSTRAINT "FK_6a72e1a5758643737cc563b96c7" FOREIGN KEY ("civil_id_front_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "juniors" ADD CONSTRAINT "FK_4662c4433223c01fe69fc1382f5" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_dfbf64ede1ff823a489902448a2"`); + await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_4662c4433223c01fe69fc1382f5"`); + await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_6a72e1a5758643737cc563b96c7"`); + await queryRunner.query(`DROP TABLE "juniors"`); + } +} diff --git a/src/db/migrations/1733732021622-create-guardian-entity.ts b/src/db/migrations/1733732021622-create-guardian-entity.ts new file mode 100644 index 0000000..ab3d334 --- /dev/null +++ b/src/db/migrations/1733732021622-create-guardian-entity.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateGuardianEntity1733732021622 implements MigrationInterface { + name = 'CreateGuardianEntity1733732021622'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "guardians" + ("id" uuid NOT NULL, + "customer_id" uuid NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "REL_6c46a1b6af00e6457cb1b70f7e" UNIQUE ("customer_id"), CONSTRAINT "PK_3dcf02f3dc96a2c017106f280be" PRIMARY KEY ("id"))`, + ); + await queryRunner.query(`ALTER TABLE "juniors" ADD "guardian_id" uuid NOT NULL`); + await queryRunner.query( + `ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "guardians" DROP CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7"`); + await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_0b11aa56264184690e2220da4a0"`); + await queryRunner.query(`ALTER TABLE "juniors" DROP COLUMN "guardian_id"`); + await queryRunner.query(`DROP TABLE "guardians"`); + } +} diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 0629239..b8656e5 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -4,3 +4,5 @@ export * from './1733209041336-create-otp-entity'; export * from './1733231692252-create-notification-settings-table'; export * from './1733298524771-create-customer-entity'; export * from './1733314952318-create-device-entity'; +export * from './1733731507261-create-junior-entity'; +export * from './1733732021622-create-guardian-entity'; diff --git a/src/document/entities/document.entity.ts b/src/document/entities/document.entity.ts index 6a9f156..2a0dc5d 100644 --- a/src/document/entities/document.entity.ts +++ b/src/document/entities/document.entity.ts @@ -1,5 +1,6 @@ import { Column, Entity, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { User } from '~/auth/entities'; +import { Junior } from '~/junior/entities'; import { DocumentType } from '../enums'; @Entity('documents') @@ -19,6 +20,12 @@ export class Document { @OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'CASCADE' }) user!: User; + @OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'CASCADE' }) + juniorCivilIdFront!: User; + + @OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'CASCADE' }) + juniorCivilIdBack!: User; + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) updatedAt!: Date; diff --git a/src/dtos/request/index.ts b/src/dtos/request/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/guardian/entities/guradian.entity.ts b/src/guardian/entities/guradian.entity.ts new file mode 100644 index 0000000..f4ed01e --- /dev/null +++ b/src/guardian/entities/guradian.entity.ts @@ -0,0 +1,35 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + JoinColumn, + OneToMany, + OneToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Customer } from '~/customer/entities'; +import { Junior } from '~/junior/entities'; + +@Entity('guardians') +export class Guardian extends BaseEntity { + @PrimaryColumn('uuid') + id!: string; + + @Column('uuid', { name: 'customer_id' }) + customerId!: string; + + @OneToOne(() => Customer, (customer) => customer.guardian, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'customer_id' }) + customer!: Customer; + + @OneToMany(() => Junior, (junior) => junior.guardian) + juniors!: Junior[]; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + updatedAt!: Date; +} diff --git a/src/guardian/guardian.module.ts b/src/guardian/guardian.module.ts new file mode 100644 index 0000000..e5b53a8 --- /dev/null +++ b/src/guardian/guardian.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Guardian } from './entities/guradian.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Guardian])], +}) +export class GuardianModule {} diff --git a/src/junior/controllers/index.ts b/src/junior/controllers/index.ts new file mode 100644 index 0000000..ddeb144 --- /dev/null +++ b/src/junior/controllers/index.ts @@ -0,0 +1 @@ +export * from './junior.controller'; diff --git a/src/junior/controllers/junior.controller.ts b/src/junior/controllers/junior.controller.ts new file mode 100644 index 0000000..32eecf3 --- /dev/null +++ b/src/junior/controllers/junior.controller.ts @@ -0,0 +1,56 @@ +import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { IJwtPayload } from '~/auth/interfaces'; +import { AuthenticatedUser } from '~/common/decorators'; +import { AccessTokenGuard } from '~/common/guards'; +import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators'; +import { PageOptionsRequestDto } from '~/core/dtos'; +import { CustomParseUUIDPipe } from '~/core/pipes'; +import { ResponseFactory } from '~/core/utils'; +import { CreateJuniorRequestDto } from '../dtos/request'; +import { JuniorResponseDto } from '../dtos/response'; +import { JuniorService } from '../services'; + +@Controller('juniors') +@ApiTags('Juniors') +@ApiBearerAuth() +export class JuniorController { + constructor(private readonly juniorService: JuniorService) {} + + @Post() + @UseGuards(AccessTokenGuard) + @ApiDataResponse(JuniorResponseDto) + async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) { + const junior = await this.juniorService.createJuniors(body, user.sub); + + return ResponseFactory.data(new JuniorResponseDto(junior)); + } + + @Get() + @UseGuards(AccessTokenGuard) + @ApiDataPageResponse(JuniorResponseDto) + async findJuniors(@AuthenticatedUser() user: IJwtPayload, @Query() pageOptions: PageOptionsRequestDto) { + const [juniors, count] = await this.juniorService.findJuniorsByGuardianId(user.sub, pageOptions); + + return ResponseFactory.dataPage( + juniors.map((juniors) => new JuniorResponseDto(juniors)), + { + page: pageOptions.page, + size: pageOptions.size, + itemCount: count, + }, + ); + } + + @Get(':juniorId') + @UseGuards(AccessTokenGuard) + @ApiDataResponse(JuniorResponseDto) + async findJuniorById( + @AuthenticatedUser() user: IJwtPayload, + @Param('juniorId', CustomParseUUIDPipe) juniorId: string, + ) { + const junior = await this.juniorService.findJuniorById(juniorId, user.sub); + + return ResponseFactory.data(new JuniorResponseDto(junior)); + } +} diff --git a/src/junior/dtos/request/create-junior-user.request.dto.ts b/src/junior/dtos/request/create-junior-user.request.dto.ts new file mode 100644 index 0000000..b86abbf --- /dev/null +++ b/src/junior/dtos/request/create-junior-user.request.dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDateString, IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator'; +import { i18nValidationMessage as i18n } from 'nestjs-i18n'; +import { COUNTRY_CODE_REGEX } from '~/auth/constants'; +import { IsValidPhoneNumber } from '~/core/decorators/validations'; + +export class CreateJuniorUserRequestDto { + @ApiProperty({ example: '+962' }) + @Matches(COUNTRY_CODE_REGEX, { + message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }), + }) + countryCode: string = '+966'; + + @ApiProperty({ example: '787259134' }) + @IsValidPhoneNumber({ + message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }), + }) + phoneNumber!: string; + + @ApiProperty({ example: 'John' }) + @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.firstName' }) }) + @IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.firstName' }) }) + firstName!: string; + + @ApiProperty({ example: 'Doe' }) + @IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.lastName' }) }) + @IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.lastName' }) }) + lastName!: string; + + @ApiProperty({ example: '2020-01-01' }) + @IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) }) + dateOfBirth!: Date; + + @ApiProperty({ example: 'test@test.com' }) + @IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) }) + email!: string; +} diff --git a/src/junior/dtos/request/create-junior.request.dto.ts b/src/junior/dtos/request/create-junior.request.dto.ts new file mode 100644 index 0000000..4ac8304 --- /dev/null +++ b/src/junior/dtos/request/create-junior.request.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsUUID } from 'class-validator'; +import { i18nValidationMessage as i18n } from 'nestjs-i18n'; +import { Relationship } from '~/junior/enums'; +import { CreateJuniorUserRequestDto } from './create-junior-user.request.dto'; +export class CreateJuniorRequestDto extends CreateJuniorUserRequestDto { + @ApiProperty({ example: Relationship.PARENT }) + @IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) }) + relationship!: Relationship; + + @ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' }) + @IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdFrontId' }) }) + civilIdFrontId!: string; + + @ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' }) + @IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdBackId' }) }) + civilIdBackId!: string; +} diff --git a/src/junior/dtos/request/index.ts b/src/junior/dtos/request/index.ts new file mode 100644 index 0000000..cfba4cb --- /dev/null +++ b/src/junior/dtos/request/index.ts @@ -0,0 +1,2 @@ +export * from './create-junior-user.request.dto'; +export * from './create-junior.request.dto'; diff --git a/src/junior/dtos/response/index.ts b/src/junior/dtos/response/index.ts new file mode 100644 index 0000000..ea31220 --- /dev/null +++ b/src/junior/dtos/response/index.ts @@ -0,0 +1 @@ +export * from './junior.response.dto'; diff --git a/src/junior/dtos/response/junior.response.dto.ts b/src/junior/dtos/response/junior.response.dto.ts new file mode 100644 index 0000000..5745df7 --- /dev/null +++ b/src/junior/dtos/response/junior.response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Junior } from '~/junior/entities'; +import { Relationship } from '~/junior/enums'; + +export class JuniorResponseDto { + @ApiProperty({ example: 'id' }) + id!: string; + + @ApiProperty({ example: 'fullName' }) + fullName!: string; + + @ApiProperty({ example: 'relationship' }) + relationship!: Relationship; + + @ApiProperty({ example: 'profilePictureId' }) + profilePictureId: string | null; + + constructor(junior: Junior) { + this.id = junior.id; + this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`; + this.relationship = junior.relationship; + this.profilePictureId = junior.customer.user.profilePictureId; + } +} diff --git a/src/junior/entities/index.ts b/src/junior/entities/index.ts new file mode 100644 index 0000000..c565d22 --- /dev/null +++ b/src/junior/entities/index.ts @@ -0,0 +1 @@ +export * from './junior.entity'; diff --git a/src/junior/entities/junior.entity.ts b/src/junior/entities/junior.entity.ts new file mode 100644 index 0000000..7e5e8d6 --- /dev/null +++ b/src/junior/entities/junior.entity.ts @@ -0,0 +1,58 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Customer } from '~/customer/entities'; +import { Document } from '~/document/entities'; +import { Guardian } from '~/guardian/entities/guradian.entity'; +import { Relationship } from '../enums'; + +@Entity('juniors') +export class Junior extends BaseEntity { + @PrimaryColumn('uuid') + id!: string; + + @Column('varchar', { length: 255 }) + relationship!: Relationship; + + @Column('uuid', { name: 'civil_id_front_id' }) + civilIdFrontId!: string; + + @Column('uuid', { name: 'civil_id_back_id' }) + civilIdBackId!: string; + + @Column('uuid', { name: 'customer_id' }) + customerId!: string; + + @Column('uuid', { name: 'guardian_id' }) + guardianId!: string; + + @OneToOne(() => Document, (document) => document.juniorCivilIdFront) + @JoinColumn({ name: 'civil_id_front_id' }) + civilIdFront!: Document; + + @OneToOne(() => Document, (document) => document.juniorCivilIdBack) + @JoinColumn({ name: 'civil_id_back_id' }) + civilIdBack!: Document; + + @OneToOne(() => Customer, (customer) => customer.junior, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'customer_id' }) + customer!: Customer; + + @ManyToOne(() => Guardian, (guardian) => guardian.juniors) + @JoinColumn({ name: 'guardian_id' }) + guardian!: Guardian; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + updatedAt!: Date; +} diff --git a/src/junior/enums/index.ts b/src/junior/enums/index.ts new file mode 100644 index 0000000..f9194f0 --- /dev/null +++ b/src/junior/enums/index.ts @@ -0,0 +1 @@ +export * from './relationship.enum'; diff --git a/src/junior/enums/relationship.enum.ts b/src/junior/enums/relationship.enum.ts new file mode 100644 index 0000000..a9d2d97 --- /dev/null +++ b/src/junior/enums/relationship.enum.ts @@ -0,0 +1,4 @@ +export enum Relationship { + PARENT = 'PARENT', + GUARDIAN = 'GUARDIAN', +} diff --git a/src/junior/junior.module.ts b/src/junior/junior.module.ts new file mode 100644 index 0000000..7ad6c64 --- /dev/null +++ b/src/junior/junior.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from '~/auth/auth.module'; +import { CustomerModule } from '~/customer/customer.module'; +import { JuniorController } from './controllers'; +import { Junior } from './entities'; +import { JuniorRepository } from './repositories'; +import { JuniorService } from './services'; + +@Module({ + controllers: [JuniorController], + providers: [JuniorService, JuniorRepository], + imports: [TypeOrmModule.forFeature([Junior]), AuthModule, CustomerModule], +}) +export class JuniorModule {} diff --git a/src/junior/repositories/index.ts b/src/junior/repositories/index.ts new file mode 100644 index 0000000..71277f9 --- /dev/null +++ b/src/junior/repositories/index.ts @@ -0,0 +1 @@ +export * from './junior.repository'; diff --git a/src/junior/repositories/junior.repository.ts b/src/junior/repositories/junior.repository.ts new file mode 100644 index 0000000..da5291d --- /dev/null +++ b/src/junior/repositories/junior.repository.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PageOptionsRequestDto } from '~/core/dtos'; +import { Junior } from '../entities/junior.entity'; +const FIRST_PAGE = 1; +@Injectable() +export class JuniorRepository { + constructor(@InjectRepository(Junior) private juniorRepository: Repository) {} + + findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) { + return this.juniorRepository.findAndCount({ + where: { guardianId }, + relations: ['customer', 'customer.user'], + skip: (pageOptions.page - FIRST_PAGE) * pageOptions.size, + take: pageOptions.size, + }); + } + + findJuniorById(juniorId: string, guardianId: string) { + return this.juniorRepository.findOne({ + where: { id: juniorId, guardianId }, + relations: ['customer', 'customer.user'], + }); + } +} diff --git a/src/junior/services/index.ts b/src/junior/services/index.ts new file mode 100644 index 0000000..b32073b --- /dev/null +++ b/src/junior/services/index.ts @@ -0,0 +1 @@ +export * from './junior.service'; diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts new file mode 100644 index 0000000..25caea6 --- /dev/null +++ b/src/junior/services/junior.service.ts @@ -0,0 +1,65 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { Transactional } from 'typeorm-transactional'; +import { Roles } from '~/auth/enums'; +import { UserService } from '~/auth/services'; +import { PageOptionsRequestDto } from '~/core/dtos'; +import { CustomerService } from '~/customer/services'; +import { CreateJuniorRequestDto } from '../dtos/request'; +import { Junior } from '../entities'; +import { JuniorRepository } from '../repositories'; + +@Injectable() +export class JuniorService { + constructor( + private readonly juniorRepository: JuniorRepository, + private readonly userService: UserService, + private readonly customerService: CustomerService, + ) {} + + @Transactional() + async createJuniors(body: CreateJuniorRequestDto, guardianId: string) { + const existingUser = await this.userService.findUser([{ email: body.email }, { phoneNumber: body.phoneNumber }]); + + if (existingUser) { + throw new BadRequestException('USER.ALREADY_EXISTS'); + } + + const user = await this.userService.createUser({ + email: body.email, + countryCode: body.countryCode, + phoneNumber: body.phoneNumber, + roles: [Roles.JUNIOR], + }); + + await this.customerService.createCustomer( + { + firstName: body.firstName, + lastName: body.lastName, + dateOfBirth: body.dateOfBirth, + junior: Junior.create({ + id: user.id, + guardianId, + relationship: body.relationship, + civilIdFrontId: body.civilIdFrontId, + civilIdBackId: body.civilIdBackId, + }), + }, + user, + ); + + return this.findJuniorById(user.id, guardianId); + } + + async findJuniorById(juniorId: string, guardianId: string) { + const junior = await this.juniorRepository.findJuniorById(juniorId, guardianId); + + if (!junior) { + throw new BadRequestException('JUNIOR.NOT_FOUND'); + } + return junior; + } + + findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) { + return this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions); + } +} diff --git a/src/main.ts b/src/main.ts index 7fdece5..b4f2e01 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,12 +3,20 @@ import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { Logger } from 'nestjs-pino'; +import { initializeTransactionalContext } from 'typeorm-transactional'; import { AppModule } from './app.module'; const DEFAULT_PORT = 3000; async function bootstrap() { + initializeTransactionalContext(); const app = await NestFactory.create(AppModule); app.useLogger(app.get(Logger)); + app.enableCors({ + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + preflightContinue: false, + optionsSuccessStatus: 204, + }); const config = app.get(ConfigService); const swaggerDocument = await createSwagger(app); diff --git a/typeorm.cli.ts b/typeorm.cli.ts index cd45d67..d97d709 100644 --- a/typeorm.cli.ts +++ b/typeorm.cli.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; -import { AppModule } from './src/app.module'; import { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; +import { AppModule } from './src/app.module'; /** * Getting data source through NestJS app helps in getting entities dynamically with "autoLoadEntities" NestJS feature @@ -8,6 +9,7 @@ import { DataSource } from 'typeorm'; */ async function getTypeOrmDataSource() { process.env.MIGRATIONS_RUN = 'false'; + initializeTransactionalContext(); const app = await NestFactory.createApplicationContext(AppModule);