feat: update customer profile picture and notifications settings

This commit is contained in:
Abdalhamid Alhamad
2024-12-12 13:15:47 +03:00
parent 4867a5f858
commit 51fa61dbc6
28 changed files with 150 additions and 135 deletions

View File

@ -3,18 +3,14 @@ import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { CustomerModule } from '~/customer/customer.module'; import { CustomerModule } from '~/customer/customer.module';
import { AuthController } from './controllers'; import { AuthController } from './controllers';
import { Device, User, UserNotificationSettings } from './entities'; import { Device, User } from './entities';
import { DeviceRepository, UserRepository } from './repositories'; import { DeviceRepository, UserRepository } from './repositories';
import { AuthService, DeviceService } from './services'; import { AuthService, DeviceService } from './services';
import { UserService } from './services/user.service'; import { UserService } from './services/user.service';
import { AccessTokenStrategy } from './strategies'; import { AccessTokenStrategy } from './strategies';
@Module({ @Module({
imports: [ imports: [TypeOrmModule.forFeature([User, Device]), JwtModule.register({}), forwardRef(() => CustomerModule)],
TypeOrmModule.forFeature([User, UserNotificationSettings, Device]),
JwtModule.register({}),
forwardRef(() => CustomerModule),
],
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy], providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
controllers: [AuthController], controllers: [AuthController],
exports: [UserService], exports: [UserService],

View File

@ -1,3 +1,2 @@
export * from './device.entity'; export * from './device.entity';
export * from './user-notification-settings.entity';
export * from './user.entity'; export * from './user.entity';

View File

@ -3,7 +3,6 @@ import {
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
JoinColumn,
OneToMany, OneToMany,
OneToOne, OneToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
@ -11,10 +10,8 @@ import {
} from 'typeorm'; } from 'typeorm';
import { Otp } from '~/common/modules/otp/entities'; import { Otp } from '~/common/modules/otp/entities';
import { Customer } from '~/customer/entities/customer.entity'; import { Customer } from '~/customer/entities/customer.entity';
import { Document } from '~/document/entities';
import { Roles } from '../enums'; import { Roles } from '../enums';
import { Device } from './device.entity'; import { Device } from './device.entity';
import { UserNotificationSettings } from './user-notification-settings.entity';
@Entity('users') @Entity('users')
export class User extends BaseEntity { export class User extends BaseEntity {
@ -48,22 +45,9 @@ export class User extends BaseEntity {
@Column('text', { nullable: true, array: true, name: 'roles' }) @Column('text', { nullable: true, array: true, name: 'roles' })
roles!: Roles[]; roles!: Roles[];
@Column('varchar', { name: 'profile_picture_id', nullable: true })
profilePictureId!: string;
@OneToOne(() => Document, (document) => document.user, { cascade: true, nullable: true })
@JoinColumn({ name: 'profile_picture_id' })
profilePicture!: Document;
@OneToMany(() => Otp, (otp) => otp.user) @OneToMany(() => Otp, (otp) => otp.user)
otp!: Otp[]; otp!: Otp[];
@OneToOne(() => UserNotificationSettings, (notificationSettings) => notificationSettings.user, {
cascade: true,
eager: true,
})
notificationSettings!: UserNotificationSettings;
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true }) @OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
customer!: Customer; customer!: Customer;

View File

@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm'; import { FindOptionsWhere, Repository } from 'typeorm';
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request'; import { User } from '../entities';
import { User, UserNotificationSettings } from '../entities';
@Injectable() @Injectable()
export class UserRepository { export class UserRepository {
@ -14,7 +13,6 @@ export class UserRepository {
phoneNumber: data.phoneNumber, phoneNumber: data.phoneNumber,
countryCode: data.countryCode, countryCode: data.countryCode,
roles: data.roles, roles: data.roles,
notificationSettings: UserNotificationSettings.create(),
}), }),
); );
} }
@ -23,11 +21,6 @@ export class UserRepository {
return this.userRepository.findOne({ where }); return this.userRepository.findOne({ where });
} }
updateNotificationSettings(user: User, body: UpdateNotificationsSettingsRequestDto) {
user.notificationSettings = UserNotificationSettings.create({ ...user.notificationSettings, ...body });
return this.userRepository.save(user);
}
update(userId: string, data: Partial<User>) { update(userId: string, data: Partial<User>) {
return this.userRepository.update(userId, data); return this.userRepository.update(userId, data);
} }
@ -35,7 +28,6 @@ export class UserRepository {
createUser(data: Partial<User>) { createUser(data: Partial<User>) {
const user = this.userRepository.create({ const user = this.userRepository.create({
...data, ...data,
notificationSettings: UserNotificationSettings.create(),
}); });
return this.userRepository.save(user); return this.userRepository.save(user);

View File

@ -1,6 +1,6 @@
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm'; import { FindOptionsWhere } from 'typeorm';
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request'; import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
import { CustomerService } from '~/customer/services'; import { CustomerService } from '~/customer/services';
import { Guardian } from '~/guardian/entities/guradian.entity'; import { Guardian } from '~/guardian/entities/guradian.entity';
import { CreateUnverifiedUserRequestDto } from '../dtos/request'; import { CreateUnverifiedUserRequestDto } from '../dtos/request';
@ -15,15 +15,6 @@ export class UserService {
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService, @Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
) {} ) {}
async updateNotificationSettings(userId: string, body: UpdateNotificationsSettingsRequestDto) {
const user = await this.findUserOrThrow({ id: userId });
const notificationSettings = (await this.userRepository.updateNotificationSettings(user, body))
.notificationSettings;
return notificationSettings;
}
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) { findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
return this.userRepository.findOne(where); return this.userRepository.findOne(where);
} }
@ -74,6 +65,7 @@ export class UserService {
await this.customerService.createCustomer( await this.customerService.createCustomer(
{ {
guardian: Guardian.create({ id: user.id }), guardian: Guardian.create({ id: user.id }),
notificationSettings: new CustomerNotificationSettings(),
}, },
user, user,
); );

View File

@ -1,9 +1,9 @@
import { Body, Controller, Patch, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Roles } from '~/auth/enums';
import { IJwtPayload } from '~/auth/interfaces'; import { IJwtPayload } from '~/auth/interfaces';
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators'; import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard, RolesGuard } from '~/common/guards'; import { AccessTokenGuard } from '~/common/guards';
import { ApiDataResponse } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils'; import { ResponseFactory } from '~/core/utils';
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request'; import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response'; import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
@ -15,9 +15,18 @@ import { CustomerService } from '../services';
export class CustomerController { export class CustomerController {
constructor(private readonly customerService: CustomerService) {} constructor(private readonly customerService: CustomerService) {}
@Get('/profile')
@UseGuards(AccessTokenGuard)
@ApiDataResponse(CustomerResponseDto)
async getCustomerProfile(@AuthenticatedUser() { sub }: IJwtPayload) {
const customer = await this.customerService.findCustomerById(sub);
return ResponseFactory.data(new CustomerResponseDto(customer));
}
@Patch('') @Patch('')
@UseGuards(RolesGuard) @UseGuards(AccessTokenGuard)
@AllowedRoles(Roles.GUARDIAN) @ApiDataResponse(CustomerResponseDto)
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) { async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
const customer = await this.customerService.updateCustomer(sub, body); const customer = await this.customerService.updateCustomer(sub, body);
@ -26,6 +35,7 @@ export class CustomerController {
@Patch('settings/notifications') @Patch('settings/notifications')
@UseGuards(AccessTokenGuard) @UseGuards(AccessTokenGuard)
@ApiDataResponse(NotificationSettingsResponseDto)
async updateNotificationSettings( async updateNotificationSettings(
@AuthenticatedUser() { sub }: IJwtPayload, @AuthenticatedUser() { sub }: IJwtPayload,
@Body() body: UpdateNotificationsSettingsRequestDto, @Body() body: UpdateNotificationsSettingsRequestDto,

View File

@ -3,11 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '~/auth/auth.module'; import { AuthModule } from '~/auth/auth.module';
import { CustomerController } from './controllers'; import { CustomerController } from './controllers';
import { Customer } from './entities'; import { Customer } from './entities';
import { CustomerNotificationSettings } from './entities/customer-notification-settings.entity';
import { CustomerRepository } from './repositories/customer.repository'; import { CustomerRepository } from './repositories/customer.repository';
import { CustomerService } from './services'; import { CustomerService } from './services';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Customer]), forwardRef(() => AuthModule)], imports: [TypeOrmModule.forFeature([Customer, CustomerNotificationSettings]), forwardRef(() => AuthModule)],
controllers: [CustomerController], controllers: [CustomerController],
providers: [CustomerService, CustomerRepository], providers: [CustomerService, CustomerRepository],
exports: [CustomerService], exports: [CustomerService],

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n'; import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { IsAbove18 } from '~/core/decorators/validations'; import { IsAbove18 } from '~/core/decorators/validations';
export class UpdateCustomerRequestDto { export class UpdateCustomerRequestDto {
@ -26,4 +26,8 @@ export class UpdateCustomerRequestDto {
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) }) @IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsOptional() @IsOptional()
dateOfBirth!: Date; dateOfBirth!: Date;
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) })
profilePictureId!: string;
} }

View File

@ -1,5 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Customer } from '~/customer/entities'; import { Customer } from '~/customer/entities';
import { DocumentMetaResponseDto } from '~/document/dtos/response';
import { NotificationSettingsResponseDto } from './notification-settings.response.dto';
export class CustomerResponseDto { export class CustomerResponseDto {
@ApiProperty() @ApiProperty()
@ -50,6 +52,12 @@ export class CustomerResponseDto {
@ApiProperty() @ApiProperty()
isGuardian!: boolean; isGuardian!: boolean;
@ApiProperty()
notificationSettings!: NotificationSettingsResponseDto;
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
profilePicture!: DocumentMetaResponseDto | null;
constructor(customer: Customer) { constructor(customer: Customer) {
this.id = customer.id; this.id = customer.id;
this.customerStatus = customer.customerStatus; this.customerStatus = customer.customerStatus;
@ -67,5 +75,7 @@ export class CustomerResponseDto {
this.gender = customer.gender; this.gender = customer.gender;
this.isJunior = customer.isJunior; this.isJunior = customer.isJunior;
this.isGuardian = customer.isGuardian; this.isGuardian = customer.isGuardian;
this.notificationSettings = new NotificationSettingsResponseDto(customer.notificationSettings);
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
} }
} }

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { UserNotificationSettings } from '~/auth/entities'; import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
export class NotificationSettingsResponseDto { export class NotificationSettingsResponseDto {
@ApiProperty() @ApiProperty()
@ -11,7 +11,7 @@ export class NotificationSettingsResponseDto {
@ApiProperty() @ApiProperty()
isSmsEnabled!: boolean; isSmsEnabled!: boolean;
constructor(notificationSettings: UserNotificationSettings) { constructor(notificationSettings: CustomerNotificationSettings) {
this.isEmailEnabled = notificationSettings.isEmailEnabled; this.isEmailEnabled = notificationSettings.isEmailEnabled;
this.isPushEnabled = notificationSettings.isPushEnabled; this.isPushEnabled = notificationSettings.isPushEnabled;
this.isSmsEnabled = notificationSettings.isSmsEnabled; this.isSmsEnabled = notificationSettings.isSmsEnabled;

View File

@ -8,10 +8,10 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { User } from './user.entity'; import { Customer } from '~/customer/entities';
@Entity('user_notification_settings') @Entity('cutsomer_notification_settings')
export class UserNotificationSettings extends BaseEntity { export class CustomerNotificationSettings extends BaseEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@ -24,9 +24,9 @@ export class UserNotificationSettings extends BaseEntity {
@Column({ name: 'is_sms_enabled', default: false }) @Column({ name: 'is_sms_enabled', default: false })
isSmsEnabled!: boolean; isSmsEnabled!: boolean;
@OneToOne(() => User, (user) => user.notificationSettings, { onDelete: 'CASCADE' }) @OneToOne(() => Customer, (customer) => customer.notificationSettings, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' }) @JoinColumn({ name: 'customer_id' })
user!: User; customer!: Customer;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' }) @CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date; createdAt!: Date;

View File

@ -9,10 +9,12 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { User } from '~/auth/entities'; import { User } from '~/auth/entities';
import { Document } from '~/document/entities';
import { Guardian } from '~/guardian/entities/guradian.entity'; import { Guardian } from '~/guardian/entities/guradian.entity';
import { Junior } from '~/junior/entities'; import { Junior } from '~/junior/entities';
import { CustomerNotificationSettings } from './customer-notification-settings.entity';
@Entity() @Entity('customers')
export class Customer extends BaseEntity { export class Customer extends BaseEntity {
@PrimaryColumn('uuid') @PrimaryColumn('uuid')
id!: string; id!: string;
@ -65,6 +67,19 @@ export class Customer extends BaseEntity {
@Column('varchar', { name: 'user_id' }) @Column('varchar', { name: 'user_id' })
userId!: string; userId!: string;
@Column('varchar', { name: 'profile_picture_id', nullable: true })
profilePictureId!: string;
@OneToOne(() => CustomerNotificationSettings, (notificationSettings) => notificationSettings.customer, {
cascade: true,
eager: true,
})
notificationSettings!: CustomerNotificationSettings;
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
@JoinColumn({ name: 'profile_picture_id' })
profilePicture!: Document;
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' }) @OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' }) @JoinColumn({ name: 'user_id' })
user!: User; user!: User;

View File

@ -3,19 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm'; import { FindOptionsWhere, Repository } from 'typeorm';
import { User } from '~/auth/entities'; import { User } from '~/auth/entities';
import { Roles } from '~/auth/enums'; import { Roles } from '~/auth/enums';
import { UpdateCustomerRequestDto } from '../dtos/request'; import { UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { Customer } from '../entities'; import { Customer } from '../entities';
import { CustomerNotificationSettings } from '../entities/customer-notification-settings.entity';
@Injectable() @Injectable()
export class CustomerRepository { export class CustomerRepository {
constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {} constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {}
updateCustomer(id: string, data: UpdateCustomerRequestDto) { updateCustomer(id: string, data: Partial<Customer>) {
return this.customerRepository.update(id, data); return this.customerRepository.update(id, data);
} }
findOne(where: FindOptionsWhere<Customer>) { findOne(where: FindOptionsWhere<Customer>) {
return this.customerRepository.findOne({ where }); return this.customerRepository.findOne({ where, relations: ['profilePicture'] });
} }
createCustomer(customerData: Partial<Customer>, user: User) { createCustomer(customerData: Partial<Customer>, user: User) {
@ -29,4 +30,12 @@ export class CustomerRepository {
}), }),
); );
} }
updateNotificationSettings(customer: Customer, body: UpdateNotificationsSettingsRequestDto) {
customer.notificationSettings = CustomerNotificationSettings.create({
...customer.notificationSettings,
...body,
});
return this.customerRepository.save(customer);
}
} }

View File

@ -1,18 +1,20 @@
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { User } from '~/auth/entities'; import { User } from '~/auth/entities';
import { UserService } from '~/auth/services/user.service'; import { OciService } from '~/document/services';
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request'; import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { Customer } from '../entities'; import { Customer } from '../entities';
import { CustomerRepository } from '../repositories/customer.repository'; import { CustomerRepository } from '../repositories/customer.repository';
@Injectable() @Injectable()
export class CustomerService { export class CustomerService {
constructor( constructor(private readonly customerRepository: CustomerRepository, private readonly ociService: OciService) {}
@Inject(forwardRef(() => UserService)) private readonly userService: UserService, async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
private readonly customerRepository: CustomerRepository, const customer = await this.findCustomerById(userId);
) {}
updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) { const notificationSettings = (await this.customerRepository.updateNotificationSettings(customer, data))
return this.userService.updateNotificationSettings(userId, data); .notificationSettings;
return notificationSettings;
} }
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> { async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
@ -26,9 +28,14 @@ export class CustomerService {
async findCustomerById(id: string) { async findCustomerById(id: string) {
const customer = await this.customerRepository.findOne({ id }); const customer = await this.customerRepository.findOne({ id });
if (!customer) { if (!customer) {
throw new BadRequestException('CUSTOMER.NOT_FOUND'); throw new BadRequestException('CUSTOMER.NOT_FOUND');
} }
if (customer.profilePicture) {
customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture);
}
return customer; return customer;
} }
} }

View File

@ -9,7 +9,7 @@ export class CreateDocumentEntity1732434281561 implements MigrationInterface {
"id" uuid NOT NULL DEFAULT uuid_generate_v4(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" character varying(255) NOT NULL, "name" character varying(255) NOT NULL,
"extension" character varying(255) NOT NULL, "extension" character varying(255) NOT NULL,
"documentType" character varying(255) NOT NULL, "document_type" character varying(255) NOT NULL,
"updated_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(),
"created_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`, CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,

View File

@ -16,16 +16,10 @@ export class CreateUserEntity1733206728721 implements MigrationInterface {
"apple_id" character varying(255), "apple_id" character varying(255),
"is_profile_completed" boolean NOT NULL DEFAULT false, "is_profile_completed" boolean NOT NULL DEFAULT false,
"roles" text array, "roles" text array,
"profile_picture_id" uuid,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`, CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
); );
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {

View File

@ -1,29 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateNotificationSettingsTable1733231692252 implements MigrationInterface {
name = 'CreateNotificationSettingsTable1733231692252';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_notification_settings"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"is_email_enabled" boolean NOT NULL DEFAULT false,
"is_push_enabled" boolean NOT NULL DEFAULT false,
"is_sms_enabled" boolean NOT NULL DEFAULT false,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"user_id" uuid, CONSTRAINT "REL_52182ffd0f785e8256f8fcb4fd" UNIQUE ("user_id"),
CONSTRAINT "PK_a195de67d093e096152f387afbd" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "user_notification_settings" ADD CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_notification_settings" DROP CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6"`,
);
await queryRunner.query(`DROP TABLE "user_notification_settings"`);
}
}

View File

@ -5,7 +5,7 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query( await queryRunner.query(
`CREATE TABLE "customer" `CREATE TABLE "customers"
("id" uuid NOT NULL, ("id" uuid NOT NULL,
"customer_status" character varying(255) NOT NULL DEFAULT 'PENDING', "customer_status" character varying(255) NOT NULL DEFAULT 'PENDING',
"rejection_reason" text, "rejection_reason" text,
@ -22,19 +22,25 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
"gender" character varying(255), "gender" character varying(255),
"is_junior" boolean NOT NULL DEFAULT false, "is_junior" boolean NOT NULL DEFAULT false,
"is_guardian" boolean NOT NULL DEFAULT false, "is_guardian" boolean NOT NULL DEFAULT false,
"profile_picture_id" uuid,
"user_id" uuid NOT NULL, "user_id" uuid NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"), CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`, CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`,
); );
await queryRunner.query( await queryRunner.query(
`ALTER TABLE "customer" ADD CONSTRAINT "FK_5d1f609371a285123294fddcf3a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, `ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "customers" ADD CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
); );
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "customer" DROP CONSTRAINT "FK_5d1f609371a285123294fddcf3a"`); await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661"`);
await queryRunner.query(`DROP TABLE "customer"`); await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
await queryRunner.query(`DROP TABLE "customers"`);
} }
} }

View File

@ -24,7 +24,7 @@ export class CreateJuniorEntity1733731507261 implements MigrationInterface {
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_4662c4433223c01fe69fc1382f5" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, `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( await queryRunner.query(
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, `ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
); );
} }

View File

@ -17,7 +17,7 @@ export class CreateGuardianEntity1733732021622 implements MigrationInterface {
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, `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( await queryRunner.query(
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, `ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
); );
} }

View File

@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateCustomerNotificationsSettingsTable1733993920226 implements MigrationInterface {
name = 'CreateCustomerNotificationsSettingsTable1733993920226';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "cutsomer_notification_settings"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"is_email_enabled" boolean NOT NULL DEFAULT false,
"is_push_enabled" boolean NOT NULL DEFAULT false,
"is_sms_enabled" boolean NOT NULL DEFAULT false,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"customer_id" uuid, CONSTRAINT "REL_32f2b707407298a9eecd6cc7ea" UNIQUE ("customer_id"),
CONSTRAINT "PK_ea94fb22410c89ae6b37d63b0e3" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "cutsomer_notification_settings" ADD CONSTRAINT "FK_32f2b707407298a9eecd6cc7ea6" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "cutsomer_notification_settings" DROP CONSTRAINT "FK_32f2b707407298a9eecd6cc7ea6"`,
);
await queryRunner.query(`DROP TABLE "cutsomer_notification_settings"`);
}
}

View File

@ -1,7 +1,6 @@
export * from './1732434281561-create-document-entity'; export * from './1732434281561-create-document-entity';
export * from './1733206728721-create-user-entity'; export * from './1733206728721-create-user-entity';
export * from './1733209041336-create-otp-entity'; export * from './1733209041336-create-otp-entity';
export * from './1733231692252-create-notification-settings-table';
export * from './1733298524771-create-customer-entity'; export * from './1733298524771-create-customer-entity';
export * from './1733314952318-create-device-entity'; export * from './1733314952318-create-device-entity';
export * from './1733731507261-create-junior-entity'; export * from './1733731507261-create-junior-entity';
@ -10,3 +9,4 @@ export * from './1733748083604-create-theme-entity';
export * from './1733750228289-seed-default-avatar'; export * from './1733750228289-seed-default-avatar';
export * from './1733904556416-create-task-entities'; export * from './1733904556416-create-task-entities';
export * from './1733990253208-seeds-default-tasks-logo'; export * from './1733990253208-seeds-default-tasks-logo';
export * from './1733993920226-create-customer-notifications-settings-table';

View File

@ -18,19 +18,11 @@ export class DocumentMetaResponseDto {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
url!: string | null; url!: string | null;
@ApiProperty()
createdAt!: Date;
@ApiProperty()
updatedAt!: Date;
constructor(document: Document) { constructor(document: Document) {
this.id = document.id; this.id = document.id;
this.name = document.name; this.name = document.name;
this.extension = document.extension; this.extension = document.extension;
this.documentType = document.documentType; this.documentType = document.documentType;
this.url = document.url || null; this.url = document.url || null;
this.createdAt = document.createdAt;
this.updatedAt = document.updatedAt;
} }
} }

View File

@ -1,5 +1,6 @@
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { User } from '~/auth/entities'; import { User } from '~/auth/entities';
import { Customer } from '~/customer/entities';
import { Junior, Theme } from '~/junior/entities'; import { Junior, Theme } from '~/junior/entities';
import { Task } from '~/task/entities'; import { Task } from '~/task/entities';
import { TaskSubmission } from '~/task/entities/task-submissions.entity'; import { TaskSubmission } from '~/task/entities/task-submissions.entity';
@ -16,16 +17,16 @@ export class Document {
@Column({ type: 'varchar', length: 255 }) @Column({ type: 'varchar', length: 255 })
extension!: string; extension!: string;
@Column({ type: 'varchar', length: 255 }) @Column({ type: 'varchar', length: 255, name: 'document_type' })
documentType!: DocumentType; documentType!: DocumentType;
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'CASCADE' }) @OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
user?: User; customerPicture?: Customer;
@OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'CASCADE' }) @OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'SET NULL' })
juniorCivilIdFront?: User; juniorCivilIdFront?: User;
@OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'CASCADE' }) @OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'SET NULL' })
juniorCivilIdBack?: User; juniorCivilIdBack?: User;
@OneToMany(() => Theme, (theme) => theme.avatar) @OneToMany(() => Theme, (theme) => theme.avatar)

View File

@ -70,6 +70,7 @@ export class OciService {
return null; return null;
} }
const cachedUrl = await this.cacheService.get<string>(document.id); const cachedUrl = await this.cacheService.get<string>(document.id);
if (cachedUrl) { if (cachedUrl) {
return cachedUrl; return cachedUrl;
} }
@ -82,7 +83,7 @@ export class OciService {
this.logger.debug(`Generating pre-signed url for object ${objectName} in bucket ${bucketName}`); this.logger.debug(`Generating pre-signed url for object ${objectName} in bucket ${bucketName}`);
const res = await this.ociClient.createPreauthenticatedRequest({ const res = await this.ociClient.createPreauthenticatedRequest({
namespaceName: this.namespace, namespaceName: this.namespace,
bucketName, bucketName: 'asd',
createPreauthenticatedRequestDetails: { createPreauthenticatedRequestDetails: {
name: objectName, name: objectName,
accessType: CreatePreauthenticatedRequestDetails.AccessType.AnyObjectRead, accessType: CreatePreauthenticatedRequestDetails.AccessType.AnyObjectRead,
@ -95,7 +96,7 @@ export class OciService {
this.cacheService.set(document.id, res.preauthenticatedRequest.fullPath + objectName, '1h'); this.cacheService.set(document.id, res.preauthenticatedRequest.fullPath + objectName, '1h');
return res.preauthenticatedRequest.fullPath + objectName; return res.preauthenticatedRequest.fullPath + objectName;
} catch (error) { } catch (error) {
this.logger.error('Error generating pre-signed url', JSON.stringify(error)); this.logger.error(`Error generating pre-signed url: ${error}`);
return document.name; return document.name;
} }
} }

View File

@ -20,8 +20,8 @@ export class JuniorResponseDto {
this.id = junior.id; this.id = junior.id;
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`; this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
this.relationship = junior.relationship; this.relationship = junior.relationship;
this.profilePicture = junior.customer.user.profilePicture this.profilePicture = junior.customer.profilePicture
? new DocumentMetaResponseDto(junior.customer.user.profilePicture) ? new DocumentMetaResponseDto(junior.customer.profilePicture)
: null; : null;
} }
} }

View File

@ -3,6 +3,7 @@ import { Transactional } from 'typeorm-transactional';
import { Roles } from '~/auth/enums'; import { Roles } from '~/auth/enums';
import { UserService } from '~/auth/services'; import { UserService } from '~/auth/services';
import { PageOptionsRequestDto } from '~/core/dtos'; import { PageOptionsRequestDto } from '~/core/dtos';
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
import { CustomerService } from '~/customer/services'; import { CustomerService } from '~/customer/services';
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request'; import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
import { Junior } from '../entities'; import { Junior } from '../entities';
@ -43,6 +44,7 @@ export class JuniorService {
civilIdFrontId: body.civilIdFrontId, civilIdFrontId: body.civilIdFrontId,
civilIdBackId: body.civilIdBackId, civilIdBackId: body.civilIdBackId,
}), }),
notificationSettings: new CustomerNotificationSettings(),
}, },
user, user,
); );

View File

@ -80,7 +80,7 @@ export class TaskService {
const [imageUrl, submissionUrl, profilePictureUrl] = await Promise.all([ const [imageUrl, submissionUrl, profilePictureUrl] = await Promise.all([
this.ociService.generatePreSignedUrl(task.image), this.ociService.generatePreSignedUrl(task.image),
this.ociService.generatePreSignedUrl(task.submission?.proofOfCompletion), this.ociService.generatePreSignedUrl(task.submission?.proofOfCompletion),
this.ociService.generatePreSignedUrl(task.assignedTo.customer.user.profilePicture), this.ociService.generatePreSignedUrl(task.assignedTo.customer.profilePicture),
]); ]);
task.image.url = imageUrl; task.image.url = imageUrl;
@ -89,8 +89,8 @@ export class TaskService {
task.submission.proofOfCompletion.url = submissionUrl; task.submission.proofOfCompletion.url = submissionUrl;
} }
if (task.assignedTo.customer.user.profilePicture) { if (task.assignedTo.customer.profilePicture) {
task.assignedTo.customer.user.profilePicture.url = profilePictureUrl; task.assignedTo.customer.profilePicture.url = profilePictureUrl;
} }
}), }),
); );