mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 13:49:40 +00:00
refactor: handle kyc journey for customers
This commit is contained in:
@ -23,7 +23,8 @@
|
|||||||
"migration:generate": "npm run typeorm:cli-d migration:generate",
|
"migration:generate": "npm run typeorm:cli-d migration:generate",
|
||||||
"migration:create": "npm run typeorm:cli migration:create",
|
"migration:create": "npm run typeorm:cli migration:create",
|
||||||
"migration:up": "npm run typeorm:cli-d migration:run",
|
"migration:up": "npm run typeorm:cli-d migration:run",
|
||||||
"migration:down": "npm run typeorm:cli-d migration:revert"
|
"migration:down": "npm run typeorm:cli-d migration:revert",
|
||||||
|
"seed": "TS_NODE_PROJECT=tsconfig.json ts-node -r tsconfig-paths/register src/scripts/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abdalhamid/hello": "^2.0.0",
|
"@abdalhamid/hello": "^2.0.0",
|
||||||
|
@ -8,7 +8,7 @@ import { AuthService, Oauth2Service } from './services';
|
|||||||
import { AccessTokenStrategy } from './strategies';
|
import { AccessTokenStrategy } from './strategies';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [JwtModule.register({}), JuniorModule, UserModule, HttpModule],
|
imports: [JwtModule.register({}), UserModule, JuniorModule, HttpModule],
|
||||||
providers: [AuthService, AccessTokenStrategy, Oauth2Service],
|
providers: [AuthService, AccessTokenStrategy, Oauth2Service],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
@ -18,11 +18,11 @@ export class LoginResponseDto {
|
|||||||
user!: UserResponseDto;
|
user!: UserResponseDto;
|
||||||
|
|
||||||
@ApiProperty({ example: CustomerResponseDto })
|
@ApiProperty({ example: CustomerResponseDto })
|
||||||
customer!: CustomerResponseDto;
|
customer!: CustomerResponseDto | null;
|
||||||
|
|
||||||
constructor(IVerifyUserResponse: ILoginResponse, user: User) {
|
constructor(IVerifyUserResponse: ILoginResponse, user: User) {
|
||||||
this.user = new UserResponseDto(user);
|
this.user = new UserResponseDto(user);
|
||||||
this.customer = new CustomerResponseDto(user.customer);
|
this.customer = user.customer ? new CustomerResponseDto(user.customer) : null;
|
||||||
this.accessToken = IVerifyUserResponse.accessToken;
|
this.accessToken = IVerifyUserResponse.accessToken;
|
||||||
this.refreshToken = IVerifyUserResponse.refreshToken;
|
this.refreshToken = IVerifyUserResponse.refreshToken;
|
||||||
this.expiresAt = IVerifyUserResponse.expiresAt;
|
this.expiresAt = IVerifyUserResponse.expiresAt;
|
||||||
|
@ -21,6 +21,15 @@ export class UserResponseDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isProfileCompleted!: boolean;
|
isProfileCompleted!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isEmailEnabled!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isPushEnabled!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
roles!: Roles[];
|
roles!: Roles[];
|
||||||
|
|
||||||
@ -31,6 +40,9 @@ export class UserResponseDto {
|
|||||||
this.countryCode = user.countryCode;
|
this.countryCode = user.countryCode;
|
||||||
this.isPasswordSet = user.isPasswordSet;
|
this.isPasswordSet = user.isPasswordSet;
|
||||||
this.isProfileCompleted = user.isProfileCompleted;
|
this.isProfileCompleted = user.isProfileCompleted;
|
||||||
|
this.isSmsEnabled = user.isSmsEnabled;
|
||||||
|
this.isEmailEnabled = user.isEmailEnabled;
|
||||||
|
this.isPushEnabled = user.isPushEnabled;
|
||||||
this.roles = user.roles;
|
this.roles = user.roles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
export enum Roles {
|
export enum Roles {
|
||||||
JUNIOR = 'JUNIOR',
|
JUNIOR = 'JUNIOR',
|
||||||
GUARDIAN = 'GUARDIAN',
|
GUARDIAN = 'GUARDIAN',
|
||||||
|
CHECKER = 'CHECKER',
|
||||||
|
SUPER_ADMIN = 'SUPER_ADMIN',
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@ import { ArrayContains } from 'typeorm';
|
|||||||
import { CacheService } from '~/common/modules/cache/services';
|
import { CacheService } from '~/common/modules/cache/services';
|
||||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||||
import { OtpService } from '~/common/modules/otp/services';
|
import { OtpService } from '~/common/modules/otp/services';
|
||||||
import { JuniorTokenService } from '~/junior/services';
|
import { UserType } from '~/user/enums';
|
||||||
import { DeviceService, UserService } from '~/user/services';
|
import { DeviceService, UserService, UserTokenService } from '~/user/services';
|
||||||
import { User } from '../../user/entities';
|
import { User } from '../../user/entities';
|
||||||
import { PASSCODE_REGEX } from '../constants';
|
import { PASSCODE_REGEX } from '../constants';
|
||||||
import {
|
import {
|
||||||
@ -39,7 +39,7 @@ export class AuthService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly deviceService: DeviceService,
|
private readonly deviceService: DeviceService,
|
||||||
private readonly juniorTokenService: JuniorTokenService,
|
private readonly userTokenService: UserTokenService,
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
private readonly oauth2Service: Oauth2Service,
|
private readonly oauth2Service: Oauth2Service,
|
||||||
) {}
|
) {}
|
||||||
@ -91,13 +91,15 @@ export class AuthService {
|
|||||||
return [tokens, user];
|
return [tokens, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await this.userService.verifyUserAndCreateCustomer(user.id);
|
await this.userService.verifyPhoneNumber(user.id);
|
||||||
|
|
||||||
const tokens = await this.generateAuthToken(updatedUser);
|
await user.reload();
|
||||||
|
|
||||||
|
const tokens = await this.generateAuthToken(user);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} verified successfully`,
|
`User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} verified successfully`,
|
||||||
);
|
);
|
||||||
return [tokens, updatedUser];
|
return [tokens, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEmail(userId: string, { email }: SetEmailRequestDto) {
|
async setEmail(userId: string, { email }: SetEmailRequestDto) {
|
||||||
@ -293,11 +295,11 @@ export class AuthService {
|
|||||||
|
|
||||||
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
||||||
this.logger.log(`Setting passcode for junior with qrToken ${body.qrToken}`);
|
this.logger.log(`Setting passcode for junior with qrToken ${body.qrToken}`);
|
||||||
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
|
const juniorId = await this.userTokenService.validateToken(body.qrToken, UserType.JUNIOR);
|
||||||
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||||
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
|
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
|
||||||
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
|
await this.userService.setPasscode(juniorId!, hashedPasscode, salt);
|
||||||
await this.juniorTokenService.invalidateToken(body.qrToken);
|
await this.userTokenService.invalidateToken(body.qrToken);
|
||||||
this.logger.log(`Passcode set successfully for junior with id ${juniorId}`);
|
this.logger.log(`Passcode set successfully for junior with id ${juniorId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,4 +3,5 @@ export enum NotificationScope {
|
|||||||
TASK_COMPLETED = 'TASK_COMPLETED',
|
TASK_COMPLETED = 'TASK_COMPLETED',
|
||||||
GIFT_RECEIVED = 'GIFT_RECEIVED',
|
GIFT_RECEIVED = 'GIFT_RECEIVED',
|
||||||
OTP = 'OTP',
|
OTP = 'OTP',
|
||||||
|
USER_INVITED = 'USER_INVITED',
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MailerModule } from '@nestjs-modules/mailer';
|
import { MailerModule } from '@nestjs-modules/mailer';
|
||||||
import { Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { TwilioModule } from 'nestjs-twilio';
|
import { TwilioModule } from 'nestjs-twilio';
|
||||||
@ -21,7 +21,7 @@ import { FirebaseService, NotificationsService, TwilioService } from './services
|
|||||||
useFactory: buildMailerOptions,
|
useFactory: buildMailerOptions,
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
UserModule,
|
forwardRef(() => UserModule),
|
||||||
],
|
],
|
||||||
providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService],
|
providers: [NotificationsService, FirebaseService, NotificationsRepository, TwilioService],
|
||||||
exports: [NotificationsService],
|
exports: [NotificationsService],
|
||||||
|
@ -25,36 +25,6 @@ export class NotificationsService {
|
|||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async sendPushNotification(userId: string, title: string, body: string) {
|
|
||||||
this.logger.log(`Sending push notification to user ${userId}`);
|
|
||||||
// Get the device tokens for the user
|
|
||||||
|
|
||||||
const tokens = await this.deviceService.getTokens(userId);
|
|
||||||
|
|
||||||
if (!tokens.length) {
|
|
||||||
this.logger.log(`No device tokens found for user ${userId} but notification created in the database`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Send the notification
|
|
||||||
return this.firebaseService.sendNotification(tokens, title, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendSMS(to: string, body: string) {
|
|
||||||
this.logger.log(`Sending SMS to ${to}`);
|
|
||||||
await this.twilioService.sendSMS(to, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendEmail({ to, subject, data, template }: SendEmailRequestDto) {
|
|
||||||
this.logger.log(`Sending email to ${to}`);
|
|
||||||
await this.mailerService.sendMail({
|
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
template,
|
|
||||||
context: { ...data },
|
|
||||||
});
|
|
||||||
this.logger.log(`Email sent to ${to}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
|
async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) {
|
||||||
this.logger.log(`Getting notifications for user ${userId}`);
|
this.logger.log(`Getting notifications for user ${userId}`);
|
||||||
const [[notifications, count], unreadCount] = await Promise.all([
|
const [[notifications, count], unreadCount] = await Promise.all([
|
||||||
@ -77,6 +47,18 @@ export class NotificationsService {
|
|||||||
return this.notificationRepository.markAsRead(userId);
|
return this.notificationRepository.markAsRead(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendEmailAsync(data: SendEmailRequestDto) {
|
||||||
|
this.logger.log(`emitting ${EventType.NOTIFICATION_CREATED} event`);
|
||||||
|
const notification = await this.createNotification({
|
||||||
|
recipient: data.to,
|
||||||
|
title: data.subject,
|
||||||
|
message: '',
|
||||||
|
scope: NotificationScope.USER_INVITED,
|
||||||
|
channel: NotificationChannel.EMAIL,
|
||||||
|
});
|
||||||
|
return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification, data.data);
|
||||||
|
}
|
||||||
|
|
||||||
async sendOtpNotification(sendOtpRequest: ISendOtp, otp: string) {
|
async sendOtpNotification(sendOtpRequest: ISendOtp, otp: string) {
|
||||||
this.logger.log(`Sending OTP to ${sendOtpRequest.recipient}`);
|
this.logger.log(`Sending OTP to ${sendOtpRequest.recipient}`);
|
||||||
const notification = await this.createNotification({
|
const notification = await this.createNotification({
|
||||||
@ -92,10 +74,42 @@ export class NotificationsService {
|
|||||||
return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification);
|
return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendPushNotification(userId: string, title: string, body: string) {
|
||||||
|
this.logger.log(`Sending push notification to user ${userId}`);
|
||||||
|
// Get the device tokens for the user
|
||||||
|
|
||||||
|
const tokens = await this.deviceService.getTokens(userId);
|
||||||
|
|
||||||
|
if (!tokens.length) {
|
||||||
|
this.logger.log(`No device tokens found for user ${userId} but notification created in the database`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Send the notification
|
||||||
|
return this.firebaseService.sendNotification(tokens, title, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendSMS(to: string, body: string) {
|
||||||
|
this.logger.log(`Sending SMS to ${to}`);
|
||||||
|
await this.twilioService.sendSMS(to, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendEmail({ to, subject, data, template }: SendEmailRequestDto) {
|
||||||
|
this.logger.log(`Sending email to ${to}`);
|
||||||
|
await this.mailerService.sendMail({
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
template,
|
||||||
|
context: { ...data },
|
||||||
|
});
|
||||||
|
this.logger.log(`Email sent to ${to}`);
|
||||||
|
}
|
||||||
|
|
||||||
private getTemplateFromNotification(notification: Notification) {
|
private getTemplateFromNotification(notification: Notification) {
|
||||||
switch (notification.scope) {
|
switch (notification.scope) {
|
||||||
case NotificationScope.OTP:
|
case NotificationScope.OTP:
|
||||||
return 'otp';
|
return 'otp';
|
||||||
|
case NotificationScope.USER_INVITED:
|
||||||
|
return 'user-invite';
|
||||||
default:
|
default:
|
||||||
return 'otp';
|
return 'otp';
|
||||||
}
|
}
|
||||||
@ -115,8 +129,8 @@ export class NotificationsService {
|
|||||||
return this.sendEmail({
|
return this.sendEmail({
|
||||||
to: notification.recipient!,
|
to: notification.recipient!,
|
||||||
subject: notification.title,
|
subject: notification.title,
|
||||||
data,
|
|
||||||
template: this.getTemplateFromNotification(notification),
|
template: this.getTemplateFromNotification(notification),
|
||||||
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
71
src/common/modules/notification/templates/user-invite.hbs
Normal file
71
src/common/modules/notification/templates/user-invite.hbs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>You're Invited!</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #555;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">You're Invited to Join Us!</div>
|
||||||
|
<p class="content">
|
||||||
|
You've been invited to join our platform. Click the button below to accept the invitation and set up your account.
|
||||||
|
</p>
|
||||||
|
<a href="{{inviteLink}}" class="btn">Accept Invitation</a>
|
||||||
|
<p class="content">
|
||||||
|
If the button above doesn't work, you can copy and paste this link into your browser:<br>
|
||||||
|
<a href="{{inviteLink}}" class="link">{{inviteLink}}</a>
|
||||||
|
</p>
|
||||||
|
<div class="footer">
|
||||||
|
If you didn’t request this invitation, please ignore this email.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,13 +1,12 @@
|
|||||||
import { Body, Controller, Get, Headers, Patch, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Patch, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { IJwtPayload } from '~/auth/interfaces';
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
|
||||||
import { AuthenticatedUser } from '~/common/decorators';
|
import { AuthenticatedUser } from '~/common/decorators';
|
||||||
import { AccessTokenGuard } from '~/common/guards';
|
import { AccessTokenGuard } from '~/common/guards';
|
||||||
import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
|
import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
|
||||||
import { ResponseFactory } from '~/core/utils';
|
import { ResponseFactory } from '~/core/utils';
|
||||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
import { CreateCustomerRequestDto, UpdateCustomerRequestDto } from '../dtos/request';
|
||||||
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
|
import { CustomerResponseDto } from '../dtos/response';
|
||||||
import { CustomerService } from '../services';
|
import { CustomerService } from '../services';
|
||||||
|
|
||||||
@Controller('customers')
|
@Controller('customers')
|
||||||
@ -26,7 +25,7 @@ export class CustomerController {
|
|||||||
return ResponseFactory.data(new CustomerResponseDto(customer));
|
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('')
|
@Patch()
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
@ApiDataResponse(CustomerResponseDto)
|
@ApiDataResponse(CustomerResponseDto)
|
||||||
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
|
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
|
||||||
@ -35,16 +34,12 @@ export class CustomerController {
|
|||||||
return ResponseFactory.data(new CustomerResponseDto(customer));
|
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('settings/notifications')
|
@Post('')
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
@ApiDataResponse(NotificationSettingsResponseDto)
|
@ApiDataResponse(CustomerResponseDto)
|
||||||
async updateNotificationSettings(
|
async createCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateCustomerRequestDto) {
|
||||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
const customer = await this.customerService.createGuardianCustomer(sub, body);
|
||||||
@Body() body: UpdateNotificationsSettingsRequestDto,
|
|
||||||
@Headers(DEVICE_ID_HEADER) deviceId: string,
|
|
||||||
) {
|
|
||||||
const notificationSettings = await this.customerService.updateNotificationSettings(sub, body, deviceId);
|
|
||||||
|
|
||||||
return ResponseFactory.data(new NotificationSettingsResponseDto(notificationSettings));
|
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './customer.controller';
|
export * from './customer.controller';
|
||||||
|
export * from './internal.customer.controller';
|
||||||
|
48
src/customer/controllers/internal.customer.controller.ts
Normal file
48
src/customer/controllers/internal.customer.controller.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||||
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { CustomerFiltersRequestDto, RejectCustomerKycRequestDto } from '../dtos/request';
|
||||||
|
import { CustomerResponseDto } from '../dtos/response';
|
||||||
|
import { CustomerService } from '../services';
|
||||||
|
|
||||||
|
@ApiTags('Customers')
|
||||||
|
@Controller('internal/customers')
|
||||||
|
export class InternalCustomerController {
|
||||||
|
constructor(private readonly customerService: CustomerService) {}
|
||||||
|
@Get()
|
||||||
|
async findCustomers(@Query() filters: CustomerFiltersRequestDto) {
|
||||||
|
const [customers, count] = await this.customerService.findCustomers(filters);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
customers.map((customer) => new CustomerResponseDto(customer)),
|
||||||
|
{
|
||||||
|
page: filters.page,
|
||||||
|
size: filters.size,
|
||||||
|
itemCount: count,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':customerId')
|
||||||
|
async findCustomerById(@Param('customerId', CustomParseUUIDPipe) customerId: string) {
|
||||||
|
const customer = await this.customerService.findCustomerById(customerId);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':customerId/approve')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async approveKycForCustomer(@Param('customerId', CustomParseUUIDPipe) customerId: string) {
|
||||||
|
await this.customerService.approveKycForCustomer(customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':customerId/reject')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async rejectKycForCustomer(
|
||||||
|
@Param('customerId', CustomParseUUIDPipe) customerId: string,
|
||||||
|
@Body() body: RejectCustomerKycRequestDto,
|
||||||
|
) {
|
||||||
|
await this.customerService.rejectKycForCustomer(customerId, body);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
import { forwardRef, Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { GuardianModule } from '~/guardian/guardian.module';
|
||||||
import { UserModule } from '~/user/user.module';
|
import { UserModule } from '~/user/user.module';
|
||||||
import { CustomerController } from './controllers';
|
import { CustomerController, InternalCustomerController } 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, CustomerNotificationSettings]), forwardRef(() => UserModule)],
|
imports: [TypeOrmModule.forFeature([Customer]), forwardRef(() => UserModule), GuardianModule],
|
||||||
controllers: [CustomerController],
|
controllers: [CustomerController, InternalCustomerController],
|
||||||
providers: [CustomerService, CustomerRepository],
|
providers: [CustomerService, CustomerRepository],
|
||||||
exports: [CustomerService],
|
exports: [CustomerService],
|
||||||
})
|
})
|
||||||
|
39
src/customer/dtos/request/create-customer.request.dto.ts
Normal file
39
src/customer/dtos/request/create-customer.request.dto.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { IsAbove18 } from '~/core/decorators/validations';
|
||||||
|
export class CreateCustomerRequestDto {
|
||||||
|
@ApiProperty({ example: 'John' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
|
||||||
|
@IsOptional()
|
||||||
|
firstName!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Doe' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
|
||||||
|
@IsOptional()
|
||||||
|
lastName!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'JO' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.countryOfResidence' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.countryOfResidence' }) })
|
||||||
|
@IsOptional()
|
||||||
|
countryOfResidence!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2021-01-01' })
|
||||||
|
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
|
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
|
@IsOptional()
|
||||||
|
dateOfBirth!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdFrontId' }) })
|
||||||
|
@IsOptional()
|
||||||
|
civilIdFrontId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdBackId' }) })
|
||||||
|
@IsOptional()
|
||||||
|
civilIdBackId!: string;
|
||||||
|
}
|
23
src/customer/dtos/request/customer-filters.request.dto.ts
Normal file
23
src/customer/dtos/request/customer-filters.request.dto.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { KycStatus } from '~/customer/enums';
|
||||||
|
|
||||||
|
export class CustomerFiltersRequestDto extends PageOptionsRequestDto {
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.name' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.name' }) })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiPropertyOptional({ description: 'search by name' })
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsEnum(KycStatus, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.kycStatus' }) })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: KycStatus,
|
||||||
|
enumName: 'KycStatus',
|
||||||
|
example: KycStatus.PENDING,
|
||||||
|
description: 'kyc status of the customer',
|
||||||
|
})
|
||||||
|
kycStatus?: string;
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
|
export * from './create-customer.request.dto';
|
||||||
|
export * from './customer-filters.request.dto';
|
||||||
|
export * from './reject-customer-kyc.request.dto';
|
||||||
export * from './update-customer.request.dto';
|
export * from './update-customer.request.dto';
|
||||||
export * from './update-notifications-settings.request.dto';
|
|
||||||
|
11
src/customer/dtos/request/reject-customer-kyc.request.dto.ts
Normal file
11
src/customer/dtos/request/reject-customer-kyc.request.dto.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
export class RejectCustomerKycRequestDto {
|
||||||
|
@ApiPropertyOptional({ description: 'reason for rejecting the customer kyc' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.rejectionReason' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.rejectionReason' }) })
|
||||||
|
@IsOptional({ message: i18n('validation.IsOptional', { path: 'general', property: 'customer.rejectionReason' }) })
|
||||||
|
reason?: string;
|
||||||
|
}
|
@ -1,32 +1,8 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, PartialType } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
import { IsOptional, 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 { CreateCustomerRequestDto } from './create-customer.request.dto';
|
||||||
export class UpdateCustomerRequestDto {
|
export class UpdateCustomerRequestDto extends PartialType(CreateCustomerRequestDto) {
|
||||||
@ApiProperty({ example: 'John' })
|
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
|
|
||||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
|
|
||||||
@IsOptional()
|
|
||||||
firstName!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Doe' })
|
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
|
|
||||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
|
|
||||||
@IsOptional()
|
|
||||||
lastName!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'JO' })
|
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.countryOfResidence' }) })
|
|
||||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.countryOfResidence' }) })
|
|
||||||
@IsOptional()
|
|
||||||
countryOfResidence!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: '2021-01-01' })
|
|
||||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
|
||||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
|
||||||
@IsOptional()
|
|
||||||
dateOfBirth!: Date;
|
|
||||||
|
|
||||||
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
|
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
|
||||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) })
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
|
import { CustomerStatus, KycStatus } from '~/customer/enums';
|
||||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||||
import { NotificationSettingsResponseDto } from './notification-settings.response.dto';
|
|
||||||
|
|
||||||
export class CustomerResponseDto {
|
export class CustomerResponseDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
customerStatus!: string;
|
customerStatus!: CustomerStatus;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
rejectionReason!: string;
|
kycStatus!: KycStatus;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
rejectionReason!: string | null;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
firstName!: string;
|
firstName!: string;
|
||||||
@ -52,15 +55,13 @@ export class CustomerResponseDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
isGuardian!: boolean;
|
isGuardian!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
notificationSettings!: NotificationSettingsResponseDto;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
|
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
|
||||||
profilePicture!: DocumentMetaResponseDto | null;
|
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;
|
||||||
|
this.kycStatus = customer.kycStatus;
|
||||||
this.rejectionReason = customer.rejectionReason;
|
this.rejectionReason = customer.rejectionReason;
|
||||||
this.firstName = customer.firstName;
|
this.firstName = customer.firstName;
|
||||||
this.lastName = customer.lastName;
|
this.lastName = customer.lastName;
|
||||||
@ -75,7 +76,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;
|
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export * from './customer-response.dto';
|
export * from './customer-response.dto';
|
||||||
export * from './notification-settings.response.dto';
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
|
||||||
|
|
||||||
export class NotificationSettingsResponseDto {
|
|
||||||
@ApiProperty()
|
|
||||||
isEmailEnabled!: boolean;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
isPushEnabled!: boolean;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
isSmsEnabled!: boolean;
|
|
||||||
|
|
||||||
constructor(notificationSettings: CustomerNotificationSettings) {
|
|
||||||
this.isEmailEnabled = notificationSettings.isEmailEnabled;
|
|
||||||
this.isPushEnabled = notificationSettings.isPushEnabled;
|
|
||||||
this.isSmsEnabled = notificationSettings.isSmsEnabled;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import {
|
|
||||||
BaseEntity,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
Entity,
|
|
||||||
JoinColumn,
|
|
||||||
OneToOne,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { Customer } from '~/customer/entities';
|
|
||||||
|
|
||||||
@Entity('cutsomer_notification_settings')
|
|
||||||
export class CustomerNotificationSettings extends BaseEntity {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id!: string;
|
|
||||||
|
|
||||||
@Column({ name: 'is_email_enabled', default: false })
|
|
||||||
isEmailEnabled!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: 'is_push_enabled', default: false })
|
|
||||||
isPushEnabled!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: 'is_sms_enabled', default: false })
|
|
||||||
isSmsEnabled!: boolean;
|
|
||||||
|
|
||||||
@OneToOne(() => Customer, (customer) => customer.notificationSettings, { onDelete: 'CASCADE' })
|
|
||||||
@JoinColumn({ name: 'customer_id' })
|
|
||||||
customer!: Customer;
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
|
||||||
createdAt!: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone' })
|
|
||||||
updatedAt!: Date;
|
|
||||||
}
|
|
@ -12,18 +12,21 @@ 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 { User } from '~/user/entities';
|
import { User } from '~/user/entities';
|
||||||
import { CustomerNotificationSettings } from './customer-notification-settings.entity';
|
import { CustomerStatus, KycStatus } from '../enums';
|
||||||
|
|
||||||
@Entity('customers')
|
@Entity('customers')
|
||||||
export class Customer extends BaseEntity {
|
export class Customer extends BaseEntity {
|
||||||
@PrimaryColumn('uuid')
|
@PrimaryColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Column('varchar', { length: 255, default: 'PENDING', name: 'customer_status' })
|
@Column('varchar', { length: 255, default: CustomerStatus.PENDING, name: 'customer_status' })
|
||||||
customerStatus!: string;
|
customerStatus!: CustomerStatus;
|
||||||
|
|
||||||
|
@Column('varchar', { length: 255, default: KycStatus.PENDING, name: 'kyc_status' })
|
||||||
|
kycStatus!: KycStatus;
|
||||||
|
|
||||||
@Column('text', { nullable: true, name: 'rejection_reason' })
|
@Column('text', { nullable: true, name: 'rejection_reason' })
|
||||||
rejectionReason!: string;
|
rejectionReason!: string | null;
|
||||||
|
|
||||||
@Column('varchar', { length: 255, nullable: true, name: 'first_name' })
|
@Column('varchar', { length: 255, nullable: true, name: 'first_name' })
|
||||||
firstName!: string;
|
firstName!: string;
|
||||||
@ -70,12 +73,6 @@ export class Customer extends BaseEntity {
|
|||||||
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
||||||
profilePictureId!: string;
|
profilePictureId!: string;
|
||||||
|
|
||||||
@OneToOne(() => CustomerNotificationSettings, (notificationSettings) => notificationSettings.customer, {
|
|
||||||
cascade: true,
|
|
||||||
eager: true,
|
|
||||||
})
|
|
||||||
notificationSettings!: CustomerNotificationSettings;
|
|
||||||
|
|
||||||
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
|
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
|
||||||
@JoinColumn({ name: 'profile_picture_id' })
|
@JoinColumn({ name: 'profile_picture_id' })
|
||||||
profilePicture!: Document;
|
profilePicture!: Document;
|
||||||
@ -90,6 +87,20 @@ export class Customer extends BaseEntity {
|
|||||||
@OneToOne(() => Guardian, (guardian) => guardian.customer, { cascade: true })
|
@OneToOne(() => Guardian, (guardian) => guardian.customer, { cascade: true })
|
||||||
guardian!: Guardian;
|
guardian!: Guardian;
|
||||||
|
|
||||||
|
@Column('uuid', { name: 'civil_id_front_id' })
|
||||||
|
civilIdFrontId!: string;
|
||||||
|
|
||||||
|
@Column('uuid', { name: 'civil_id_back_id' })
|
||||||
|
civilIdBackId!: string;
|
||||||
|
|
||||||
|
@OneToOne(() => Document, (document) => document.customerCivilIdFront)
|
||||||
|
@JoinColumn({ name: 'civil_id_front_id' })
|
||||||
|
civilIdFront!: Document;
|
||||||
|
|
||||||
|
@OneToOne(() => Document, (document) => document.customerCivilIdBack)
|
||||||
|
@JoinColumn({ name: 'civil_id_back_id' })
|
||||||
|
civilIdBack!: Document;
|
||||||
|
|
||||||
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
5
src/customer/enums/customer-status.enum.ts
Normal file
5
src/customer/enums/customer-status.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum CustomerStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
APPROVED = 'APPROVED',
|
||||||
|
REJECTED = 'REJECTED',
|
||||||
|
}
|
2
src/customer/enums/index.ts
Normal file
2
src/customer/enums/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './customer-status.enum';
|
||||||
|
export * from './kyc-status.enum';
|
5
src/customer/enums/kyc-status.enum.ts
Normal file
5
src/customer/enums/kyc-status.enum.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum KycStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
APPROVED = 'APPROVED',
|
||||||
|
REJECTED = 'REJECTED',
|
||||||
|
}
|
@ -1,12 +1,9 @@
|
|||||||
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 { Guardian } from '~/guardian/entities/guradian.entity';
|
|
||||||
import { CreateJuniorRequestDto } from '~/junior/dtos/request';
|
import { CreateJuniorRequestDto } from '~/junior/dtos/request';
|
||||||
import { Junior } from '~/junior/entities';
|
import { CreateCustomerRequestDto, CustomerFiltersRequestDto } 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 {
|
||||||
@ -20,44 +17,52 @@ export class CustomerRepository {
|
|||||||
return this.customerRepository.findOne({ where, relations: ['profilePicture'] });
|
return this.customerRepository.findOne({ where, relations: ['profilePicture'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
createJuniorCustomer(guardianId: string, userId: string, body: CreateJuniorRequestDto) {
|
createCustomer(userId: string, body: CreateCustomerRequestDto | CreateJuniorRequestDto, isGuardian: boolean = false) {
|
||||||
return this.customerRepository.save(
|
return this.customerRepository.save(
|
||||||
this.customerRepository.create({
|
this.customerRepository.create({
|
||||||
|
id: userId,
|
||||||
|
userId,
|
||||||
|
isGuardian,
|
||||||
|
isJunior: !isGuardian,
|
||||||
firstName: body.firstName,
|
firstName: body.firstName,
|
||||||
lastName: body.lastName,
|
lastName: body.lastName,
|
||||||
dateOfBirth: body.dateOfBirth,
|
dateOfBirth: body.dateOfBirth,
|
||||||
junior: Junior.create({
|
civilIdFrontId: body.civilIdFrontId,
|
||||||
id: userId,
|
civilIdBackId: body.civilIdBackId,
|
||||||
guardianId,
|
|
||||||
relationship: body.relationship,
|
|
||||||
civilIdFrontId: body.civilIdFrontId,
|
|
||||||
civilIdBackId: body.civilIdBackId,
|
|
||||||
}),
|
|
||||||
id: userId,
|
|
||||||
userId,
|
|
||||||
isJunior: true,
|
|
||||||
notificationSettings: new CustomerNotificationSettings(),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createGuardianCustomer(userId: string) {
|
findCustomers(filters: CustomerFiltersRequestDto) {
|
||||||
return this.customerRepository.save(
|
const query = this.customerRepository.createQueryBuilder('customer');
|
||||||
this.customerRepository.create({
|
|
||||||
notificationSettings: new CustomerNotificationSettings(),
|
if (filters.name) {
|
||||||
guardian: Guardian.create({ id: userId }),
|
const nameParts = filters.name.trim().split(/\s+/);
|
||||||
id: userId,
|
console.log(nameParts);
|
||||||
userId,
|
nameParts.length > 1
|
||||||
isGuardian: true,
|
? query.andWhere('customer.firstName LIKE :firstName AND customer.lastName LIKE :lastName', {
|
||||||
}),
|
firstName: `%${nameParts[0]}%`,
|
||||||
);
|
lastName: `%${nameParts[1]}%`,
|
||||||
|
})
|
||||||
|
: query.andWhere('customer.firstName LIKE :name OR customer.lastName LIKE :name', {
|
||||||
|
name: `%${filters.name.trim()}%`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.kycStatus) {
|
||||||
|
query.andWhere('customer.kycStatus = :kycStatus', { kycStatus: filters.kycStatus });
|
||||||
|
}
|
||||||
|
|
||||||
|
query.orderBy('customer.createdAt', 'DESC');
|
||||||
|
query.take(filters.size);
|
||||||
|
query.skip((filters.page - 1) * filters.size);
|
||||||
|
|
||||||
|
return query.getManyAndCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNotificationSettings(customer: Customer, body: UpdateNotificationsSettingsRequestDto) {
|
findCustomerByCivilId(civilIdFrontId: string, civilIdBackId: string) {
|
||||||
customer.notificationSettings = CustomerNotificationSettings.create({
|
return this.customerRepository.findOne({
|
||||||
...customer.notificationSettings,
|
where: [{ civilIdFrontId, civilIdBackId }, { civilIdFrontId }, { civilIdBackId }],
|
||||||
...body,
|
|
||||||
});
|
});
|
||||||
return this.customerRepository.save(customer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Transactional } from 'typeorm-transactional';
|
||||||
import { DocumentService, OciService } from '~/document/services';
|
import { DocumentService, OciService } from '~/document/services';
|
||||||
|
import { GuardianService } from '~/guardian/services';
|
||||||
import { CreateJuniorRequestDto } from '~/junior/dtos/request';
|
import { CreateJuniorRequestDto } from '~/junior/dtos/request';
|
||||||
import { DeviceService } from '~/user/services';
|
import {
|
||||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
CreateCustomerRequestDto,
|
||||||
|
CustomerFiltersRequestDto,
|
||||||
|
RejectCustomerKycRequestDto,
|
||||||
|
UpdateCustomerRequestDto,
|
||||||
|
} from '../dtos/request';
|
||||||
import { Customer } from '../entities';
|
import { Customer } from '../entities';
|
||||||
|
import { KycStatus } from '../enums';
|
||||||
import { CustomerRepository } from '../repositories/customer.repository';
|
import { CustomerRepository } from '../repositories/customer.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -12,26 +19,9 @@ export class CustomerService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly customerRepository: CustomerRepository,
|
private readonly customerRepository: CustomerRepository,
|
||||||
private readonly ociService: OciService,
|
private readonly ociService: OciService,
|
||||||
private readonly deviceService: DeviceService,
|
|
||||||
private readonly documentService: DocumentService,
|
private readonly documentService: DocumentService,
|
||||||
|
private readonly guardianService: GuardianService,
|
||||||
) {}
|
) {}
|
||||||
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId: string) {
|
|
||||||
this.logger.log(`Updating notification settings for user ${userId}`);
|
|
||||||
const customer = await this.findCustomerById(userId);
|
|
||||||
|
|
||||||
const notificationSettings = (await this.customerRepository.updateNotificationSettings(customer, data))
|
|
||||||
.notificationSettings;
|
|
||||||
|
|
||||||
if (data.isPushEnabled && deviceId) {
|
|
||||||
this.logger.log(`Updating device ${deviceId} with fcmToken`);
|
|
||||||
await this.deviceService.updateDevice(deviceId, {
|
|
||||||
fcmToken: data.fcmToken,
|
|
||||||
userId: userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.logger.log(`Notification settings updated for user ${userId}`);
|
|
||||||
return notificationSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
|
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
|
||||||
this.logger.log(`Updating customer ${userId}`);
|
this.logger.log(`Updating customer ${userId}`);
|
||||||
@ -42,14 +32,12 @@ export class CustomerService {
|
|||||||
return this.findCustomerById(userId);
|
return this.findCustomerById(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
createJuniorCustomer(guardianId: string, juniorId: string, body: CreateJuniorRequestDto) {
|
async createJuniorCustomer(guardianId: string, juniorId: string, body: CreateJuniorRequestDto) {
|
||||||
this.logger.log(`Creating customer for user ${juniorId}`);
|
this.logger.log(`Creating junior customer for user ${juniorId}`);
|
||||||
return this.customerRepository.createJuniorCustomer(guardianId, juniorId, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
createGuardianCustomer(userId: string) {
|
await this.validateCivilIdForCustomer(guardianId, body.civilIdFrontId, body.civilIdBackId);
|
||||||
this.logger.log(`Creating guardian customer for user ${userId}`);
|
|
||||||
return this.customerRepository.createGuardianCustomer(userId);
|
return this.customerRepository.createCustomer(juniorId, body, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCustomerById(id: string) {
|
async findCustomerById(id: string) {
|
||||||
@ -69,6 +57,61 @@ export class CustomerService {
|
|||||||
return customer;
|
return customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async approveKycForCustomer(customerId: string) {
|
||||||
|
const customer = await this.findCustomerById(customerId);
|
||||||
|
|
||||||
|
if (customer.kycStatus === KycStatus.APPROVED) {
|
||||||
|
this.logger.error(`Customer ${customerId} is already approved`);
|
||||||
|
throw new BadRequestException('CUSTOMER.ALREADY_APPROVED');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Approving KYC for customer ${customerId}`);
|
||||||
|
await this.customerRepository.updateCustomer(customerId, { kycStatus: KycStatus.APPROVED, rejectionReason: null });
|
||||||
|
this.logger.log(`KYC approved for customer ${customerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findCustomers(filters: CustomerFiltersRequestDto) {
|
||||||
|
this.logger.log(`Finding customers with filters ${JSON.stringify(filters)}`);
|
||||||
|
return this.customerRepository.findCustomers(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async createGuardianCustomer(userId: string, body: CreateCustomerRequestDto) {
|
||||||
|
this.logger.log(`Creating guardian customer for user ${userId}`);
|
||||||
|
const existingCustomer = await this.customerRepository.findOne({ id: userId });
|
||||||
|
|
||||||
|
if (existingCustomer) {
|
||||||
|
this.logger.error(`Customer ${userId} already exists`);
|
||||||
|
throw new BadRequestException('CUSTOMER.ALRADY_EXISTS');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId);
|
||||||
|
|
||||||
|
const customer = await this.customerRepository.createCustomer(userId, body, true);
|
||||||
|
this.logger.log(`customer created for user ${userId}`);
|
||||||
|
|
||||||
|
await this.guardianService.createGuardian(customer.id);
|
||||||
|
this.logger.log(`Guardian created for customer ${customer.id}`);
|
||||||
|
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectKycForCustomer(customerId: string, { reason }: RejectCustomerKycRequestDto) {
|
||||||
|
const customer = await this.findCustomerById(customerId);
|
||||||
|
|
||||||
|
if (customer.kycStatus === KycStatus.REJECTED) {
|
||||||
|
this.logger.error(`Customer ${customerId} is already rejected`);
|
||||||
|
throw new BadRequestException('CUSTOMER.ALREADY_REJECTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Rejecting KYC for customer ${customerId}`);
|
||||||
|
await this.customerRepository.updateCustomer(customerId, {
|
||||||
|
kycStatus: KycStatus.REJECTED,
|
||||||
|
rejectionReason: reason,
|
||||||
|
});
|
||||||
|
this.logger.log(`KYC rejected for customer ${customerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
private async validateProfilePictureForCustomer(userId: string, profilePictureId?: string) {
|
private async validateProfilePictureForCustomer(userId: string, profilePictureId?: string) {
|
||||||
if (!profilePictureId) return;
|
if (!profilePictureId) return;
|
||||||
|
|
||||||
@ -86,4 +129,37 @@ export class CustomerService {
|
|||||||
throw new BadRequestException('DOCUMENT.NOT_CREATED_BY_USER');
|
throw new BadRequestException('DOCUMENT.NOT_CREATED_BY_USER');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validateCivilIdForCustomer(userId: string, civilIdFrontId: string, civilIdBackId: string) {
|
||||||
|
this.logger.log(`Validating customer documents`);
|
||||||
|
|
||||||
|
if (!civilIdFrontId || !civilIdBackId) {
|
||||||
|
this.logger.error('Civil id front and back are required');
|
||||||
|
throw new BadRequestException('CUSTOMER.CIVIL_ID_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [civilIdFront, civilIdBack] = await Promise.all([
|
||||||
|
this.documentService.findDocumentById(civilIdFrontId),
|
||||||
|
this.documentService.findDocumentById(civilIdBackId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!civilIdFront || !civilIdBack) {
|
||||||
|
this.logger.error('Civil id front or back not found');
|
||||||
|
throw new BadRequestException('CUSTOMER.CIVIL_ID_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (civilIdFront.createdById !== userId || civilIdBack.createdById !== userId) {
|
||||||
|
this.logger.error(`Civil id front or back not created by user with id ${userId}`);
|
||||||
|
throw new BadRequestException('CUSTOMER.CIVIL_ID_NOT_CREATED_BY_USER');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerWithTheSameId = await this.customerRepository.findCustomerByCivilId(civilIdFrontId, civilIdBackId);
|
||||||
|
|
||||||
|
if (customerWithTheSameId) {
|
||||||
|
this.logger.error(
|
||||||
|
`Customer with civil id front ${civilIdFrontId} and civil id back ${civilIdBackId} already exists`,
|
||||||
|
);
|
||||||
|
throw new BadRequestException('CUSTOMER.CIVIL_ID_ALREADY_EXISTS');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddKycStatusToCustomer1739868002943 implements MigrationInterface {
|
||||||
|
name = 'AddKycStatusToCustomer1739868002943';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "customers" ADD "kyc_status" character varying(255) NOT NULL DEFAULT 'PENDING'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "kyc_status"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddCivilidToCustomersAndUpdateNotificationsSettings1739954239949 implements MigrationInterface {
|
||||||
|
name = 'AddCivilidToCustomersAndUpdateNotificationsSettings1739954239949'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_4662c4433223c01fe69fc1382f5"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_6a72e1a5758643737cc563b96c7"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "REL_6a72e1a5758643737cc563b96c"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" DROP COLUMN "civil_id_front_id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "REL_4662c4433223c01fe69fc1382f"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" DROP COLUMN "civil_id_back_id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" ADD "civil_id_front_id" uuid NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" ADD CONSTRAINT "UQ_d5f99c497892ce31598ba19a72c" UNIQUE ("civil_id_front_id")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" ADD "civil_id_back_id" uuid NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" ADD CONSTRAINT "UQ_2191662d124c56dd968ba01bf18" UNIQUE ("civil_id_back_id")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "is_email_enabled" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "is_push_enabled" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "is_sms_enabled" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" ADD CONSTRAINT "FK_d5f99c497892ce31598ba19a72c" FOREIGN KEY ("civil_id_front_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" ADD CONSTRAINT "FK_2191662d124c56dd968ba01bf18" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_2191662d124c56dd968ba01bf18"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_d5f99c497892ce31598ba19a72c"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "is_sms_enabled"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "is_push_enabled"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "is_email_enabled"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "UQ_2191662d124c56dd968ba01bf18"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "civil_id_back_id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "UQ_d5f99c497892ce31598ba19a72c"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN "civil_id_front_id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" ADD "civil_id_back_id" uuid NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" ADD CONSTRAINT "REL_4662c4433223c01fe69fc1382f" UNIQUE ("civil_id_back_id")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" ADD "civil_id_front_id" uuid NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "juniors" ADD CONSTRAINT "REL_6a72e1a5758643737cc563b96c" UNIQUE ("civil_id_front_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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateUserRegistrationTable1740045960580 implements MigrationInterface {
|
||||||
|
name = 'CreateUserRegistrationTable1740045960580'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "user_registration_tokens" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying(255) NOT NULL, "user_type" character varying(255) NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "expiry_date" TIMESTAMP NOT NULL, "user_id" uuid, "junior_id" uuid, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_5881556d05b46fc7bd9e3bba935" UNIQUE ("token"), CONSTRAINT "PK_135a2d86443071ff0ba1c14135c" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_5881556d05b46fc7bd9e3bba93" ON "user_registration_tokens" ("token") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_registration_tokens" ADD CONSTRAINT "FK_57cbbe079a7945d6ed1df114825" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_registration_tokens" ADD CONSTRAINT "FK_e41bec3ed6e549cbf90f57cc344" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_registration_tokens" DROP CONSTRAINT "FK_e41bec3ed6e549cbf90f57cc344"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_registration_tokens" DROP CONSTRAINT "FK_57cbbe079a7945d6ed1df114825"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_5881556d05b46fc7bd9e3bba93"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_registration_tokens"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -19,3 +19,6 @@ export * from './1734861516657-create-gift-entities';
|
|||||||
export * from './1734944692999-create-notification-entity-and-edit-device';
|
export * from './1734944692999-create-notification-entity-and-edit-device';
|
||||||
export * from './1736414850257-add-flags-to-user-entity';
|
export * from './1736414850257-add-flags-to-user-entity';
|
||||||
export * from './1736753223884-add_created_by_to_document_table';
|
export * from './1736753223884-add_created_by_to_document_table';
|
||||||
|
export * from './1739868002943-add-kyc-status-to-customer';
|
||||||
|
export * from './1739954239949-add-civilid-to-customers-and-update-notifications-settings';
|
||||||
|
export * from './1740045960580-create-user-registration-table';
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Gift } from '~/gift/entities';
|
import { Gift } from '~/gift/entities';
|
||||||
import { Junior, Theme } from '~/junior/entities';
|
import { Theme } from '~/junior/entities';
|
||||||
import { SavingGoal } from '~/saving-goals/entities';
|
import { SavingGoal } from '~/saving-goals/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';
|
||||||
@ -37,11 +37,11 @@ export class Document {
|
|||||||
@OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
|
@OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
|
||||||
customerPicture?: Customer;
|
customerPicture?: Customer;
|
||||||
|
|
||||||
@OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'SET NULL' })
|
@OneToOne(() => Customer, (customer) => customer.civilIdFront, { onDelete: 'SET NULL' })
|
||||||
juniorCivilIdFront?: User;
|
customerCivilIdFront?: User;
|
||||||
|
|
||||||
@OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'SET NULL' })
|
@OneToOne(() => Customer, (customer) => customer.civilIdBack, { onDelete: 'SET NULL' })
|
||||||
juniorCivilIdBack?: User;
|
customerCivilIdBack?: User;
|
||||||
|
|
||||||
@OneToMany(() => Theme, (theme) => theme.avatar)
|
@OneToMany(() => Theme, (theme) => theme.avatar)
|
||||||
themes?: Theme[];
|
themes?: Theme[];
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Guardian } from './entities/guradian.entity';
|
import { Guardian } from './entities/guradian.entity';
|
||||||
|
import { GuardianRepository } from './repositories';
|
||||||
|
import { GuardianService } from './services';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
providers: [GuardianService, GuardianRepository],
|
||||||
imports: [TypeOrmModule.forFeature([Guardian])],
|
imports: [TypeOrmModule.forFeature([Guardian])],
|
||||||
|
exports: [GuardianService],
|
||||||
})
|
})
|
||||||
export class GuardianModule {}
|
export class GuardianModule {}
|
||||||
|
18
src/guardian/repositories/guardian.repository.ts
Normal file
18
src/guardian/repositories/guardian.repository.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Guardian } from '../entities/guradian.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GuardianRepository {
|
||||||
|
constructor(@InjectRepository(Guardian) private readonly guardianRepository: Repository<Guardian>) {}
|
||||||
|
|
||||||
|
createGuardian(customerId: string) {
|
||||||
|
return this.guardianRepository.save(
|
||||||
|
this.guardianRepository.create({
|
||||||
|
id: customerId,
|
||||||
|
customerId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
1
src/guardian/repositories/index.ts
Normal file
1
src/guardian/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './guardian.repository';
|
11
src/guardian/services/guardian.service.ts
Normal file
11
src/guardian/services/guardian.service.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { GuardianRepository } from '../repositories';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GuardianService {
|
||||||
|
constructor(private readonly guardianRepository: GuardianRepository) {}
|
||||||
|
|
||||||
|
createGuardian(customerId: string) {
|
||||||
|
return this.guardianRepository.createGuardian(customerId);
|
||||||
|
}
|
||||||
|
}
|
1
src/guardian/services/index.ts
Normal file
1
src/guardian/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './guardian.service';
|
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"customer": {
|
"customer": {
|
||||||
|
"name": "الاسم",
|
||||||
"firstName": "الاسم الأول",
|
"firstName": "الاسم الأول",
|
||||||
"lastName": "اسم العائلة",
|
"lastName": "اسم العائلة",
|
||||||
"countryOfResidence": "بلد الإقامة",
|
"countryOfResidence": "بلد الإقامة",
|
||||||
@ -31,7 +32,8 @@
|
|||||||
"profilePictureId": "معرّف صورة الملف الشخصي",
|
"profilePictureId": "معرّف صورة الملف الشخصي",
|
||||||
"isEmailEnabled": "هل البريد الإلكتروني مفعّل",
|
"isEmailEnabled": "هل البريد الإلكتروني مفعّل",
|
||||||
"isSmsEnabled": "هل الرسائل النصية مفعّلة",
|
"isSmsEnabled": "هل الرسائل النصية مفعّلة",
|
||||||
"isPushEnabled": "هل الإشعارات مفعّلة"
|
"isPushEnabled": "هل الإشعارات مفعّلة",
|
||||||
|
"kycStatus": "حالة التحقق من الهوية"
|
||||||
},
|
},
|
||||||
|
|
||||||
"junior": {
|
"junior": {
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"customer": {
|
"customer": {
|
||||||
|
"name": "Name",
|
||||||
"firstName": "First name",
|
"firstName": "First name",
|
||||||
"lastName": "Last name",
|
"lastName": "Last name",
|
||||||
"countryOfResidence": "Country of residence",
|
"countryOfResidence": "Country of residence",
|
||||||
@ -32,7 +33,8 @@
|
|||||||
"profilePictureId": "Profile picture ID",
|
"profilePictureId": "Profile picture ID",
|
||||||
"isEmailEnabled": "Is Email enabled",
|
"isEmailEnabled": "Is Email enabled",
|
||||||
"isSmsEnabled": "Is SMS enabled",
|
"isSmsEnabled": "Is SMS enabled",
|
||||||
"isPushEnabled": "Is Push enabled"
|
"isPushEnabled": "Is Push enabled",
|
||||||
|
"kycStatus": "KYC status"
|
||||||
},
|
},
|
||||||
|
|
||||||
"junior": {
|
"junior": {
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
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: 'customer.firstName' }) })
|
|
||||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
|
|
||||||
firstName!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: 'Doe' })
|
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
|
|
||||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.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;
|
|
||||||
}
|
|
@ -1,9 +1,40 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsUUID } from 'class-validator';
|
import { IsDateString, IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator';
|
||||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
|
||||||
|
import { IsValidPhoneNumber } from '~/core/decorators/validations';
|
||||||
import { Relationship } from '~/junior/enums';
|
import { Relationship } from '~/junior/enums';
|
||||||
import { CreateJuniorUserRequestDto } from './create-junior-user.request.dto';
|
export class CreateJuniorRequestDto {
|
||||||
export class CreateJuniorRequestDto extends 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: 'customer.firstName' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
|
||||||
|
firstName!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Doe' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.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;
|
||||||
|
|
||||||
@ApiProperty({ example: Relationship.PARENT })
|
@ApiProperty({ example: Relationship.PARENT })
|
||||||
@IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) })
|
@IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) })
|
||||||
relationship!: Relationship;
|
relationship!: Relationship;
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './create-junior-user.request.dto';
|
|
||||||
export * from './create-junior.request.dto';
|
export * from './create-junior.request.dto';
|
||||||
export * from './set-theme.request.dto';
|
export * from './set-theme.request.dto';
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export * from './junior-registration-token.entity';
|
|
||||||
export * from './junior.entity';
|
export * from './junior.entity';
|
||||||
export * from './theme.entity';
|
export * from './theme.entity';
|
||||||
|
@ -12,14 +12,13 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Allowance } from '~/allowance/entities';
|
import { Allowance } from '~/allowance/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Document } from '~/document/entities';
|
|
||||||
import { Gift } from '~/gift/entities';
|
import { Gift } from '~/gift/entities';
|
||||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
import { MoneyRequest } from '~/money-request/entities';
|
import { MoneyRequest } from '~/money-request/entities';
|
||||||
import { Category, SavingGoal } from '~/saving-goals/entities';
|
import { Category, SavingGoal } from '~/saving-goals/entities';
|
||||||
import { Task } from '~/task/entities';
|
import { Task } from '~/task/entities';
|
||||||
|
import { UserRegistrationToken } from '~/user/entities';
|
||||||
import { Relationship } from '../enums';
|
import { Relationship } from '../enums';
|
||||||
import { JuniorRegistrationToken } from './junior-registration-token.entity';
|
|
||||||
import { Theme } from './theme.entity';
|
import { Theme } from './theme.entity';
|
||||||
|
|
||||||
@Entity('juniors')
|
@Entity('juniors')
|
||||||
@ -30,26 +29,12 @@ export class Junior extends BaseEntity {
|
|||||||
@Column('varchar', { length: 255 })
|
@Column('varchar', { length: 255 })
|
||||||
relationship!: Relationship;
|
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' })
|
@Column('uuid', { name: 'customer_id' })
|
||||||
customerId!: string;
|
customerId!: string;
|
||||||
|
|
||||||
@Column('uuid', { name: 'guardian_id' })
|
@Column('uuid', { name: 'guardian_id' })
|
||||||
guardianId!: string;
|
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' })
|
@OneToOne(() => Customer, (customer) => customer.junior, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'customer_id' })
|
@JoinColumn({ name: 'customer_id' })
|
||||||
customer!: Customer;
|
customer!: Customer;
|
||||||
@ -70,8 +55,8 @@ export class Junior extends BaseEntity {
|
|||||||
@OneToMany(() => Category, (category) => category.junior)
|
@OneToMany(() => Category, (category) => category.junior)
|
||||||
categories!: Category[];
|
categories!: Category[];
|
||||||
|
|
||||||
@OneToMany(() => JuniorRegistrationToken, (token) => token.junior)
|
@OneToMany(() => UserRegistrationToken, (token) => token.junior)
|
||||||
registrationTokens!: JuniorRegistrationToken[];
|
registrationTokens!: UserRegistrationToken[];
|
||||||
|
|
||||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
|
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
|
||||||
moneyRequests!: MoneyRequest[];
|
moneyRequests!: MoneyRequest[];
|
||||||
|
@ -4,21 +4,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { CustomerModule } from '~/customer/customer.module';
|
import { CustomerModule } from '~/customer/customer.module';
|
||||||
import { UserModule } from '~/user/user.module';
|
import { UserModule } from '~/user/user.module';
|
||||||
import { JuniorController } from './controllers';
|
import { JuniorController } from './controllers';
|
||||||
import { Junior, JuniorRegistrationToken, Theme } from './entities';
|
import { Junior, Theme } from './entities';
|
||||||
import { JuniorRepository, JuniorTokenRepository } from './repositories';
|
import { JuniorRepository } from './repositories';
|
||||||
import { BranchIoService, JuniorService, JuniorTokenService, QrcodeService } from './services';
|
import { BranchIoService, JuniorService, QrcodeService } from './services';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [JuniorController],
|
controllers: [JuniorController],
|
||||||
providers: [
|
providers: [JuniorService, JuniorRepository, QrcodeService, BranchIoService],
|
||||||
JuniorService,
|
imports: [TypeOrmModule.forFeature([Junior, Theme]), UserModule, CustomerModule, HttpModule],
|
||||||
JuniorRepository,
|
exports: [JuniorService],
|
||||||
JuniorTokenService,
|
|
||||||
JuniorTokenRepository,
|
|
||||||
QrcodeService,
|
|
||||||
BranchIoService,
|
|
||||||
],
|
|
||||||
imports: [TypeOrmModule.forFeature([Junior, Theme, JuniorRegistrationToken]), UserModule, CustomerModule, HttpModule],
|
|
||||||
exports: [JuniorService, JuniorTokenService],
|
|
||||||
})
|
})
|
||||||
export class JuniorModule {}
|
export class JuniorModule {}
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export * from './junior-token.repository';
|
|
||||||
export * from './junior.repository';
|
export * from './junior.repository';
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { JuniorRegistrationToken } from '../entities';
|
|
||||||
const TOKEN_LENGTH = 16;
|
|
||||||
@Injectable()
|
|
||||||
export class JuniorTokenRepository {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(JuniorRegistrationToken)
|
|
||||||
private readonly juniorTokenRepository: Repository<JuniorRegistrationToken>,
|
|
||||||
) {}
|
|
||||||
generateToken(juniorId: string) {
|
|
||||||
return this.juniorTokenRepository.save(
|
|
||||||
this.juniorTokenRepository.create({
|
|
||||||
juniorId,
|
|
||||||
expiryDate: moment().add('15', 'minutes').toDate(),
|
|
||||||
token: `${moment().unix()}-${crypto.randomBytes(TOKEN_LENGTH).toString('hex')}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
findByToken(token: string) {
|
|
||||||
return this.juniorTokenRepository.findOne({ where: { token, isUsed: false } });
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidateToken(token: string) {
|
|
||||||
return this.juniorTokenRepository.update({ token }, { isUsed: true });
|
|
||||||
}
|
|
||||||
}
|
|
@ -43,9 +43,12 @@ export class JuniorRepository {
|
|||||||
return this.juniorRepository.manager.findOne(Theme, { where: { juniorId }, relations: ['avatar'] });
|
return this.juniorRepository.manager.findOne(Theme, { where: { juniorId }, relations: ['avatar'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
findJuniorByCivilId(civilIdFrontId: string, civilIdBackId: string) {
|
createJunior(userId: string, data: Partial<Junior>) {
|
||||||
return this.juniorRepository.findOne({
|
return this.juniorRepository.save(
|
||||||
where: [{ civilIdFrontId, civilIdBackId }, { civilIdFrontId }, { civilIdBackId }],
|
this.juniorRepository.create({
|
||||||
});
|
id: userId,
|
||||||
|
...data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export * from './branch-io.service';
|
export * from './branch-io.service';
|
||||||
export * from './junior-token.service';
|
|
||||||
export * from './junior.service';
|
export * from './junior.service';
|
||||||
export * from './qrcode.service';
|
export * from './qrcode.service';
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { JuniorTokenRepository } from '../repositories';
|
|
||||||
import { QrcodeService } from './qrcode.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JuniorTokenService {
|
|
||||||
private readonly logger = new Logger(JuniorTokenService.name);
|
|
||||||
constructor(
|
|
||||||
private readonly juniorTokenRepository: JuniorTokenRepository,
|
|
||||||
private readonly qrCodeService: QrcodeService,
|
|
||||||
) {}
|
|
||||||
async generateToken(juniorId: string): Promise<string> {
|
|
||||||
this.logger.log(`Generating token for junior ${juniorId}`);
|
|
||||||
const tokenEntity = await this.juniorTokenRepository.generateToken(juniorId);
|
|
||||||
this.logger.log(`Token generated successfully for junior ${juniorId}`);
|
|
||||||
return this.qrCodeService.generateQrCode(tokenEntity.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateToken(token: string) {
|
|
||||||
this.logger.log(`Validating token ${token}`);
|
|
||||||
const tokenEntity = await this.juniorTokenRepository.findByToken(token);
|
|
||||||
|
|
||||||
if (!tokenEntity) {
|
|
||||||
this.logger.error(`Token ${token} not found`);
|
|
||||||
throw new BadRequestException('TOKEN.INVALID');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenEntity.expiryDate < new Date()) {
|
|
||||||
this.logger.error(`Token ${token} expired`);
|
|
||||||
throw new BadRequestException('TOKEN.EXPIRED');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Token validated successfully`);
|
|
||||||
return tokenEntity.juniorId;
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidateToken(token: string) {
|
|
||||||
this.logger.log(`Invalidating token ${token}`);
|
|
||||||
return this.juniorTokenRepository.invalidateToken(token);
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,36 +4,40 @@ import { Roles } from '~/auth/enums';
|
|||||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
import { CustomerService } from '~/customer/services';
|
import { CustomerService } from '~/customer/services';
|
||||||
import { DocumentService, OciService } from '~/document/services';
|
import { DocumentService, OciService } from '~/document/services';
|
||||||
|
import { UserType } from '~/user/enums';
|
||||||
import { UserService } from '~/user/services';
|
import { UserService } from '~/user/services';
|
||||||
|
import { UserTokenService } from '~/user/services/user-token.service';
|
||||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||||
import { Junior } from '../entities';
|
import { Junior } from '../entities';
|
||||||
import { JuniorRepository } from '../repositories';
|
import { JuniorRepository } from '../repositories';
|
||||||
import { JuniorTokenService } from './junior-token.service';
|
import { QrcodeService } from './qrcode.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JuniorService {
|
export class JuniorService {
|
||||||
private readonly logger = new Logger(JuniorService.name);
|
private readonly logger = new Logger(JuniorService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly juniorRepository: JuniorRepository,
|
private readonly juniorRepository: JuniorRepository,
|
||||||
private readonly juniorTokenService: JuniorTokenService,
|
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
|
private readonly userTokenService: UserTokenService,
|
||||||
private readonly customerService: CustomerService,
|
private readonly customerService: CustomerService,
|
||||||
private readonly documentService: DocumentService,
|
private readonly documentService: DocumentService,
|
||||||
private readonly ociService: OciService,
|
private readonly ociService: OciService,
|
||||||
|
private readonly qrCodeService: QrcodeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async createJuniors(body: CreateJuniorRequestDto, guardianId: string) {
|
async createJuniors(body: CreateJuniorRequestDto, guardianId: string) {
|
||||||
this.logger.log(`Creating junior for guardian ${guardianId}`);
|
this.logger.log(`Creating junior for guardian ${guardianId}`);
|
||||||
const existingUser = await this.userService.findUser([{ email: body.email }, { phoneNumber: body.phoneNumber }]);
|
const existingUser = await this.userService.findUser([
|
||||||
|
{ email: body.email },
|
||||||
|
{ phoneNumber: body.phoneNumber, countryCode: body.countryCode },
|
||||||
|
]);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
this.logger.error(`User with email ${body.email} or phone number ${body.phoneNumber} already exists`);
|
this.logger.error(`User with email ${body.email} or phone number ${body.phoneNumber} already exists`);
|
||||||
throw new BadRequestException('USER.ALREADY_EXISTS');
|
throw new BadRequestException('USER.ALREADY_EXISTS');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.validateJuniorDocuments(guardianId, body.civilIdFrontId, body.civilIdBackId);
|
|
||||||
|
|
||||||
const user = await this.userService.createUser({
|
const user = await this.userService.createUser({
|
||||||
email: body.email,
|
email: body.email,
|
||||||
countryCode: body.countryCode,
|
countryCode: body.countryCode,
|
||||||
@ -41,11 +45,17 @@ export class JuniorService {
|
|||||||
roles: [Roles.JUNIOR],
|
roles: [Roles.JUNIOR],
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.customerService.createJuniorCustomer(guardianId, user.id, body);
|
const customer = await this.customerService.createJuniorCustomer(guardianId, user.id, body);
|
||||||
|
|
||||||
|
await this.juniorRepository.createJunior(user.id, {
|
||||||
|
guardianId,
|
||||||
|
relationship: body.relationship,
|
||||||
|
customerId: customer.id,
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.log(`Junior ${user.id} created successfully`);
|
this.logger.log(`Junior ${user.id} created successfully`);
|
||||||
|
|
||||||
return this.juniorTokenService.generateToken(user.id);
|
return this.generateToken(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) {
|
async findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) {
|
||||||
@ -90,13 +100,16 @@ export class JuniorService {
|
|||||||
|
|
||||||
async validateToken(token: string) {
|
async validateToken(token: string) {
|
||||||
this.logger.log(`Validating token ${token}`);
|
this.logger.log(`Validating token ${token}`);
|
||||||
const juniorId = await this.juniorTokenService.validateToken(token);
|
const juniorId = await this.userTokenService.validateToken(token, UserType.JUNIOR);
|
||||||
return this.findJuniorById(juniorId, true);
|
return this.findJuniorById(juniorId!, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateToken(juniorId: string) {
|
async generateToken(juniorId: string) {
|
||||||
this.logger.log(`Generating token for junior ${juniorId}`);
|
this.logger.log(`Generating token for junior ${juniorId}`);
|
||||||
return this.juniorTokenService.generateToken(juniorId);
|
|
||||||
|
const token = await this.userTokenService.generateToken(juniorId, UserType.JUNIOR);
|
||||||
|
|
||||||
|
return this.qrCodeService.generateQrCode(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
async doesJuniorBelongToGuardian(guardianId: string, juniorId: string) {
|
async doesJuniorBelongToGuardian(guardianId: string, juniorId: string) {
|
||||||
@ -106,38 +119,6 @@ export class JuniorService {
|
|||||||
return !!junior;
|
return !!junior;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateJuniorDocuments(userId: string, civilIdFrontId: string, civilIdBackId: string) {
|
|
||||||
this.logger.log(`Validating junior documents`);
|
|
||||||
if (!civilIdFrontId || !civilIdBackId) {
|
|
||||||
this.logger.error('Civil id front and back are required');
|
|
||||||
throw new BadRequestException('JUNIOR.CIVIL_ID_REQUIRED');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [civilIdFront, civilIdBack] = await Promise.all([
|
|
||||||
this.documentService.findDocumentById(civilIdFrontId),
|
|
||||||
this.documentService.findDocumentById(civilIdBackId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!civilIdFront || !civilIdBack) {
|
|
||||||
this.logger.error('Civil id front or back not found');
|
|
||||||
throw new BadRequestException('JUNIOR.CIVIL_ID_REQUIRED');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (civilIdFront.createdById !== userId || civilIdBack.createdById !== userId) {
|
|
||||||
this.logger.error(`Civil id front or back not created by user with id ${userId}`);
|
|
||||||
throw new BadRequestException('JUNIOR.CIVIL_ID_NOT_CREATED_BY_GUARDIAN');
|
|
||||||
}
|
|
||||||
|
|
||||||
const juniorWithSameCivilId = await this.juniorRepository.findJuniorByCivilId(civilIdFrontId, civilIdBackId);
|
|
||||||
|
|
||||||
if (juniorWithSameCivilId) {
|
|
||||||
this.logger.error(
|
|
||||||
`Junior with civil id front ${civilIdFrontId} and civil id back ${civilIdBackId} already exists`,
|
|
||||||
);
|
|
||||||
throw new BadRequestException('JUNIOR.CIVIL_ID_ALREADY_EXISTS');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async prepareJuniorImages(juniors: Junior[]) {
|
private async prepareJuniorImages(juniors: Junior[]) {
|
||||||
this.logger.log(`Preparing junior images`);
|
this.logger.log(`Preparing junior images`);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
49
src/scripts/seed.ts
Normal file
49
src/scripts/seed.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
console.log('Starting seeder script...');
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error('Uncaught Exception:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
});
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { UserRepository } from '..//user/repositories';
|
||||||
|
import { AppModule } from '../app.module';
|
||||||
|
import { Roles } from '../auth/enums';
|
||||||
|
import { User } from '../user/entities';
|
||||||
|
import { initializeTransactionalContext } from 'typeorm-transactional';
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
initializeTransactionalContext();
|
||||||
|
const app = await NestFactory.createApplicationContext(AppModule);
|
||||||
|
const userRepository = app.get<Repository<User>>('UserRepository');
|
||||||
|
const adminEmail = process.env.SUPER_ADMIN_EMAIL;
|
||||||
|
const adminPassword = process.env.SUPER_ADMIN_PASSWORD;
|
||||||
|
if (!adminEmail || !adminPassword) {
|
||||||
|
console.error('Missing SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD in .env file');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const existingAdmin = await userRepository.findOne({ where: { email: adminEmail } });
|
||||||
|
if (!existingAdmin) {
|
||||||
|
const hashedPassword = await bcrypt.hash(adminPassword, SALT_ROUNDS);
|
||||||
|
const superAdmin = userRepository.create({
|
||||||
|
email: adminEmail,
|
||||||
|
password: hashedPassword,
|
||||||
|
roles: [Roles.SUPER_ADMIN],
|
||||||
|
isEmailVerified: true,
|
||||||
|
isPhoneVerified: true,
|
||||||
|
});
|
||||||
|
await userRepository.save(superAdmin);
|
||||||
|
console.log('Super Admin created successfully.');
|
||||||
|
} else {
|
||||||
|
console.log('Super Admin already exists.');
|
||||||
|
}
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
50
src/user/controllers/admin.user.controller.ts
Normal file
50
src/user/controllers/admin.user.controller.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { UserResponseDto } from '~/auth/dtos/response';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { AllowedRoles } from '~/common/decorators';
|
||||||
|
import { RolesGuard } from '~/common/guards';
|
||||||
|
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||||
|
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||||
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { CreateCheckerRequestDto, UserFiltersRequestDto } from '../dtos/request';
|
||||||
|
import { UserService } from '../services';
|
||||||
|
|
||||||
|
@Controller('admin/users')
|
||||||
|
@ApiTags('Users')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.SUPER_ADMIN)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AdminUserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
@Post()
|
||||||
|
@ApiDataResponse(UserResponseDto)
|
||||||
|
async createCheckers(@Body() data: CreateCheckerRequestDto) {
|
||||||
|
const user = await this.userService.createChecker(data);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new UserResponseDto(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiDataPageResponse(UserResponseDto)
|
||||||
|
async findUsers(@Query() filters: UserFiltersRequestDto) {
|
||||||
|
const [users, count] = await this.userService.findUsers(filters);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
users.map((user) => new UserResponseDto(user)),
|
||||||
|
{
|
||||||
|
page: filters.page,
|
||||||
|
size: filters.size,
|
||||||
|
itemCount: count,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':userId')
|
||||||
|
@ApiDataResponse(UserResponseDto)
|
||||||
|
async findUserById(@Param('userId', CustomParseUUIDPipe) userId: string) {
|
||||||
|
const user = await this.userService.findUserOrThrow({ id: userId });
|
||||||
|
|
||||||
|
return ResponseFactory.data(new UserResponseDto(user));
|
||||||
|
}
|
||||||
|
}
|
2
src/user/controllers/index.ts
Normal file
2
src/user/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './admin.user.controller';
|
||||||
|
export * from './user.controller';
|
32
src/user/controllers/user.controller.ts
Normal file
32
src/user/controllers/user.controller.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Body, Controller, Headers, HttpCode, HttpStatus, Patch, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||||
|
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||||
|
import { AccessTokenGuard } from '~/common/guards';
|
||||||
|
import { SetInternalPasswordRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||||
|
import { UserService } from '../services';
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
@ApiTags('Users')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class UserController {
|
||||||
|
constructor(private userService: UserService) {}
|
||||||
|
|
||||||
|
@Patch('notifications-settings')
|
||||||
|
async updateNotificationSettings(
|
||||||
|
@AuthenticatedUser() user: IJwtPayload,
|
||||||
|
@Body() data: UpdateNotificationsSettingsRequestDto,
|
||||||
|
@Headers(DEVICE_ID_HEADER) deviceId: string,
|
||||||
|
) {
|
||||||
|
return this.userService.updateNotificationSettings(user.sub, data, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('internal/set-password')
|
||||||
|
@Public()
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async setPassword(@Body() data: SetInternalPasswordRequestDto) {
|
||||||
|
return this.userService.setCheckerPassword(data);
|
||||||
|
}
|
||||||
|
}
|
25
src/user/dtos/request/create-checker.request.dto.ts
Normal file
25
src/user/dtos/request/create-checker.request.dto.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEmail, 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 CreateCheckerRequestDto {
|
||||||
|
@ApiProperty({ example: 'checker@example.com' })
|
||||||
|
@ApiProperty({ example: 'test@test.com' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.email' }) })
|
||||||
|
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '+962' })
|
||||||
|
@Matches(COUNTRY_CODE_REGEX, {
|
||||||
|
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
|
||||||
|
})
|
||||||
|
countryCode!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '797229134' })
|
||||||
|
@IsValidPhoneNumber({
|
||||||
|
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
|
||||||
|
})
|
||||||
|
phoneNumber!: string;
|
||||||
|
}
|
4
src/user/dtos/request/index.ts
Normal file
4
src/user/dtos/request/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './create-checker.request.dto';
|
||||||
|
export * from './set-internal-password.request.dto';
|
||||||
|
export * from './update-notifications-settings.request.dto';
|
||||||
|
export * from './user-filters.request.dto';
|
15
src/user/dtos/request/set-internal-password.request.dto.ts
Normal file
15
src/user/dtos/request/set-internal-password.request.dto.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
|
||||||
|
export class SetInternalPasswordRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.token' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.token' }) })
|
||||||
|
token!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
|
||||||
|
password!: string;
|
||||||
|
}
|
@ -3,22 +3,22 @@ import { IsBoolean, IsOptional, IsString, ValidateIf } from 'class-validator';
|
|||||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
export class UpdateNotificationsSettingsRequestDto {
|
export class UpdateNotificationsSettingsRequestDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isEmailEnabled' }) })
|
@IsBoolean({ message: i18n('validation.IsBoolean', { path: 'general', property: 'customer.isEmailEnabled' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isEmailEnabled!: boolean;
|
isEmailEnabled!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isPushEnabled' }) })
|
@IsBoolean({ message: i18n('validation.IsBoolean', { path: 'general', property: 'customer.isPushEnabled' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isPushEnabled!: boolean;
|
isPushEnabled!: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean({ message: i18n('validation.isBoolean', { path: 'general', property: 'customer.isSmsEnabled' }) })
|
@IsBoolean({ message: i18n('validation.IsBoolean', { path: 'general', property: 'customer.isSmsEnabled' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isSmsEnabled!: boolean;
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
@IsString({ message: i18n('validation.isString', { path: 'general', property: 'auth.fcmToken' }) })
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||||
@ValidateIf((o) => o.isPushEnabled)
|
@ValidateIf((o) => o.isPushEnabled)
|
||||||
fcmToken?: string;
|
fcmToken?: string;
|
||||||
}
|
}
|
18
src/user/dtos/request/user-filters.request.dto.ts
Normal file
18
src/user/dtos/request/user-filters.request.dto.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
|
||||||
|
export class UserFiltersRequestDto extends PageOptionsRequestDto {
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'user.search' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'user.search' }) })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiPropertyOptional({ description: 'Search by email or phone number' })
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsEnum(Roles, { message: i18n('validation.IsEnum', { path: 'general', property: 'user.role' }) })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiPropertyOptional({ enum: Roles, enumName: 'Roles', example: Roles.CHECKER, description: 'Role of the user' })
|
||||||
|
role?: string;
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
export * from './device.entity';
|
export * from './device.entity';
|
||||||
|
export * from './user-registration-token.entity';
|
||||||
export * from './user.entity';
|
export * from './user.entity';
|
||||||
|
@ -9,10 +9,12 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Junior } from './junior.entity';
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { UserType } from '../enums';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
@Entity('junior_registration_tokens')
|
@Entity('user_registration_tokens')
|
||||||
export class JuniorRegistrationToken extends BaseEntity {
|
export class UserRegistrationToken extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ -20,16 +22,26 @@ export class JuniorRegistrationToken extends BaseEntity {
|
|||||||
@Index()
|
@Index()
|
||||||
token!: string;
|
token!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'user_type' })
|
||||||
|
userType!: UserType;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false, name: 'is_used' })
|
@Column({ type: 'boolean', default: false, name: 'is_used' })
|
||||||
isUsed!: boolean;
|
isUsed!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'timestamp', name: 'expiry_date' })
|
@Column({ type: 'timestamp', name: 'expiry_date' })
|
||||||
expiryDate!: Date;
|
expiryDate!: Date;
|
||||||
|
|
||||||
@Column({ type: 'uuid', name: 'junior_id' })
|
@Column({ type: 'uuid', name: 'user_id', nullable: true })
|
||||||
juniorId!: string;
|
userId!: string | null;
|
||||||
|
|
||||||
@ManyToOne(() => Junior, (junior) => junior.registrationTokens)
|
@Column({ type: 'uuid', name: 'junior_id', nullable: true })
|
||||||
|
juniorId!: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.registrationTokens, { nullable: true, onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Junior, (junior) => junior.registrationTokens, { nullable: true, onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'junior_id' })
|
@JoinColumn({ name: 'junior_id' })
|
||||||
junior!: Junior;
|
junior!: Junior;
|
||||||
|
|
@ -14,6 +14,7 @@ import { Customer } from '~/customer/entities/customer.entity';
|
|||||||
import { Document } from '~/document/entities';
|
import { Document } from '~/document/entities';
|
||||||
import { Roles } from '../../auth/enums';
|
import { Roles } from '../../auth/enums';
|
||||||
import { Device } from './device.entity';
|
import { Device } from './device.entity';
|
||||||
|
import { UserRegistrationToken } from './user-registration-token.entity';
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
@ -50,6 +51,15 @@ export class User extends BaseEntity {
|
|||||||
@Column('boolean', { default: false, name: 'is_profile_completed' })
|
@Column('boolean', { default: false, name: 'is_profile_completed' })
|
||||||
isProfileCompleted!: boolean;
|
isProfileCompleted!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'is_email_enabled', default: false })
|
||||||
|
isEmailEnabled!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'is_push_enabled', default: false })
|
||||||
|
isPushEnabled!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'is_sms_enabled', default: false })
|
||||||
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
@Column('text', { nullable: true, array: true, name: 'roles' })
|
@Column('text', { nullable: true, array: true, name: 'roles' })
|
||||||
roles!: Roles[];
|
roles!: Roles[];
|
||||||
|
|
||||||
@ -68,6 +78,9 @@ export class User extends BaseEntity {
|
|||||||
@OneToMany(() => Document, (document) => document.createdBy)
|
@OneToMany(() => Document, (document) => document.createdBy)
|
||||||
createdDocuments!: Document[];
|
createdDocuments!: Document[];
|
||||||
|
|
||||||
|
@OneToMany(() => UserRegistrationToken, (token) => token.user)
|
||||||
|
registrationTokens!: UserRegistrationToken[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
1
src/user/enums/index.ts
Normal file
1
src/user/enums/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './user-type.enum';
|
4
src/user/enums/user-type.enum.ts
Normal file
4
src/user/enums/user-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum UserType {
|
||||||
|
CHECKER = 'CHECKER',
|
||||||
|
JUNIOR = 'JUNIOR',
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
export * from './device.repository';
|
export * from './device.repository';
|
||||||
|
export * from './user-token-repository';
|
||||||
export * from './user.repository';
|
export * from './user.repository';
|
||||||
|
36
src/user/repositories/user-token-repository.ts
Normal file
36
src/user/repositories/user-token-repository.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { UserRegistrationToken } from '../entities';
|
||||||
|
import { UserType } from '../enums';
|
||||||
|
const TOKEN_LENGTH = 16;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserTokenRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(UserRegistrationToken)
|
||||||
|
private readonly userTokenRepository: Repository<UserRegistrationToken>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
generateToken(userId: string, userType: UserType, expiryDate?: Date) {
|
||||||
|
return this.userTokenRepository.save(
|
||||||
|
this.userTokenRepository.create({
|
||||||
|
userId: userType === UserType.CHECKER ? userId : null,
|
||||||
|
juniorId: userType === UserType.JUNIOR ? userId : null,
|
||||||
|
expiryDate: expiryDate ?? moment().add('15', 'minutes').toDate(),
|
||||||
|
userType,
|
||||||
|
token: `${moment().unix()}-${crypto.randomBytes(TOKEN_LENGTH).toString('hex')}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findByToken(token: string, userType: UserType) {
|
||||||
|
return this.userTokenRepository.findOne({ where: { token, isUsed: false, userType } });
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateToken(token: string) {
|
||||||
|
return this.userTokenRepository.update({ token }, { isUsed: true });
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ 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 { User } from '../../user/entities';
|
import { User } from '../../user/entities';
|
||||||
|
import { UserFiltersRequestDto } from '../dtos/request';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepository {
|
export class UserRepository {
|
||||||
@ -32,4 +33,24 @@ export class UserRepository {
|
|||||||
|
|
||||||
return this.userRepository.save(user);
|
return this.userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findUsers(filters: UserFiltersRequestDto) {
|
||||||
|
const queryBuilder = this.userRepository.createQueryBuilder('user');
|
||||||
|
|
||||||
|
if (filters.role) {
|
||||||
|
queryBuilder.andWhere(`user.roles @> ARRAY[:role]`, { role: filters.role });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
queryBuilder.andWhere(`user.email ILIKE :search OR user.phoneNumber ILIKE :search`, {
|
||||||
|
search: `%${filters.search}%`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queryBuilder.orderBy('user.createdAt', 'DESC');
|
||||||
|
queryBuilder.take(filters.size);
|
||||||
|
queryBuilder.skip((filters.page - 1) * filters.size);
|
||||||
|
|
||||||
|
return queryBuilder.getManyAndCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './device.service';
|
export * from './device.service';
|
||||||
|
export * from './user-token.service';
|
||||||
export * from './user.service';
|
export * from './user.service';
|
||||||
|
39
src/user/services/user-token.service.ts
Normal file
39
src/user/services/user-token.service.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { UserType } from '../enums';
|
||||||
|
import { UserTokenRepository } from '../repositories/user-token-repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserTokenService {
|
||||||
|
constructor(private readonly userTokenRepository: UserTokenRepository) {}
|
||||||
|
private readonly logger = new Logger(UserTokenService.name);
|
||||||
|
|
||||||
|
async generateToken(userId: string, userType: UserType, expiryDate?: Date): Promise<string> {
|
||||||
|
this.logger.log(`Generating token for user ${userId}`);
|
||||||
|
const tokenEntity = await this.userTokenRepository.generateToken(userId, userType, expiryDate);
|
||||||
|
this.logger.log(`Token generated successfully for junior ${userId}`);
|
||||||
|
return tokenEntity.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateToken(token: string, userType: UserType): Promise<string> {
|
||||||
|
this.logger.log(`Validating token ${token}`);
|
||||||
|
const tokenEntity = await this.userTokenRepository.findByToken(token, userType);
|
||||||
|
|
||||||
|
if (!tokenEntity) {
|
||||||
|
this.logger.error(`Token ${token} not found`);
|
||||||
|
throw new BadRequestException('TOKEN.INVALID');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenEntity.expiryDate < new Date()) {
|
||||||
|
this.logger.error(`Token ${token} expired`);
|
||||||
|
throw new BadRequestException('TOKEN.EXPIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Token validated successfully`);
|
||||||
|
return tokenEntity.juniorId! || tokenEntity.userId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateToken(token: string) {
|
||||||
|
this.logger.log(`Invalidating token ${token}`);
|
||||||
|
return this.userTokenRepository.invalidateToken(token);
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,34 @@
|
|||||||
import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import moment from 'moment';
|
||||||
import { FindOptionsWhere } from 'typeorm';
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
import { Transactional } from 'typeorm-transactional';
|
import { Transactional } from 'typeorm-transactional';
|
||||||
import { CustomerService } from '~/customer/services';
|
import { NotificationsService } from '~/common/modules/notification/services';
|
||||||
import { CreateUnverifiedUserRequestDto } from '../../auth/dtos/request';
|
import { CreateUnverifiedUserRequestDto } from '../../auth/dtos/request';
|
||||||
import { Roles } from '../../auth/enums';
|
import { Roles } from '../../auth/enums';
|
||||||
|
import {
|
||||||
|
CreateCheckerRequestDto,
|
||||||
|
SetInternalPasswordRequestDto,
|
||||||
|
UpdateNotificationsSettingsRequestDto,
|
||||||
|
UserFiltersRequestDto,
|
||||||
|
} from '../dtos/request';
|
||||||
import { User } from '../entities';
|
import { User } from '../entities';
|
||||||
|
import { UserType } from '../enums';
|
||||||
import { UserRepository } from '../repositories';
|
import { UserRepository } from '../repositories';
|
||||||
|
import { DeviceService } from './device.service';
|
||||||
|
import { UserTokenService } from './user-token.service';
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private readonly logger = new Logger(UserService.name);
|
private readonly logger = new Logger(UserService.name);
|
||||||
|
private adminPortalUrl = this.configService.getOrThrow('ADMIN_PORTAL_URL');
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
|
@Inject(forwardRef(() => NotificationsService)) private readonly notificationsService: NotificationsService,
|
||||||
|
private readonly deviceService: DeviceService,
|
||||||
|
private readonly userTokenService: UserTokenService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||||
@ -20,6 +36,31 @@ export class UserService {
|
|||||||
return this.userRepository.findOne(where);
|
return this.userRepository.findOne(where);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEmail(userId: string, email: string) {
|
||||||
|
this.logger.log(`Setting email ${email} for user ${userId}`);
|
||||||
|
return this.userRepository.update(userId, { email });
|
||||||
|
}
|
||||||
|
|
||||||
|
setPasscode(userId: string, passcode: string, salt: string) {
|
||||||
|
this.logger.log(`Setting passcode for user ${userId}`);
|
||||||
|
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
setPhoneNumber(userId: string, phoneNumber: string, countryCode: string) {
|
||||||
|
this.logger.log(`Setting phone number ${phoneNumber} for user ${userId}`);
|
||||||
|
return this.userRepository.update(userId, { phoneNumber, countryCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyPhoneNumber(userId: string) {
|
||||||
|
this.logger.log(`Verifying phone number for user ${userId}`);
|
||||||
|
return this.userRepository.update(userId, { isPhoneVerified: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
findUsers(filters: UserFiltersRequestDto) {
|
||||||
|
this.logger.log(`Getting users with filters ${JSON.stringify(filters)}`);
|
||||||
|
return this.userRepository.findUsers(filters);
|
||||||
|
}
|
||||||
|
|
||||||
async findUserOrThrow(where: FindOptionsWhere<User>) {
|
async findUserOrThrow(where: FindOptionsWhere<User>) {
|
||||||
this.logger.log(`Finding user with where clause ${JSON.stringify(where)}`);
|
this.logger.log(`Finding user with where clause ${JSON.stringify(where)}`);
|
||||||
const user = await this.findUser(where);
|
const user = await this.findUser(where);
|
||||||
@ -64,27 +105,50 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
setEmail(userId: string, email: string) {
|
|
||||||
this.logger.log(`Setting email ${email} for user ${userId}`);
|
|
||||||
return this.userRepository.update(userId, { email });
|
|
||||||
}
|
|
||||||
|
|
||||||
setPasscode(userId: string, passcode: string, salt: string) {
|
|
||||||
this.logger.log(`Setting passcode for user ${userId}`);
|
|
||||||
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
setPhoneNumber(userId: string, phoneNumber: string, countryCode: string) {
|
|
||||||
this.logger.log(`Setting phone number ${phoneNumber} for user ${userId}`);
|
|
||||||
return this.userRepository.update(userId, { phoneNumber, countryCode });
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyPhoneNumber(userId: string) {
|
|
||||||
this.logger.log(`Verifying phone number for user ${userId}`);
|
|
||||||
return this.userRepository.update(userId, { isPhoneVerified: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
|
async createChecker(data: CreateCheckerRequestDto) {
|
||||||
|
const existingUser = await this.userRepository.findOne([
|
||||||
|
{ email: data.email },
|
||||||
|
{ phoneNumber: data.phoneNumber, countryCode: data.countryCode },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
throw new BadRequestException('USER.ALREADY_EXISTS');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.createUser({
|
||||||
|
...data,
|
||||||
|
roles: [Roles.CHECKER],
|
||||||
|
isProfileCompleted: true,
|
||||||
|
});
|
||||||
|
const ONE_DAY = moment().add(1, 'day').toDate();
|
||||||
|
const token = await this.userTokenService.generateToken(user.id, UserType.CHECKER, ONE_DAY);
|
||||||
|
await this.sendCheckerAccountCreatedEmail(data.email, token);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId?: string) {
|
||||||
|
this.logger.log(`Updating notification settings for user ${userId} with data ${JSON.stringify(data)}`);
|
||||||
|
if (data.isPushEnabled && !data.fcmToken) {
|
||||||
|
throw new BadRequestException('USER.FCM_TOKEN_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.isPushEnabled && !deviceId) {
|
||||||
|
throw new BadRequestException('DEVICE_ID_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.isPushEnabled && deviceId && data.fcmToken) {
|
||||||
|
await this.deviceService.updateDevice(deviceId, { fcmToken: data.fcmToken, userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepository.update(userId, {
|
||||||
|
isPushEnabled: data.isPushEnabled,
|
||||||
|
isEmailEnabled: data.isEmailEnabled,
|
||||||
|
isSmsEnabled: data.isSmsEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createGoogleUser(googleId: string, email: string) {
|
async createGoogleUser(googleId: string, email: string) {
|
||||||
this.logger.log(`Creating google user with googleId ${googleId} and email ${email}`);
|
this.logger.log(`Creating google user with googleId ${googleId} and email ${email}`);
|
||||||
const user = await this.userRepository.createUser({
|
const user = await this.userRepository.createUser({
|
||||||
@ -94,12 +158,9 @@ export class UserService {
|
|||||||
isEmailVerified: true,
|
isEmailVerified: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.customerService.createGuardianCustomer(user.id);
|
|
||||||
|
|
||||||
return this.findUserOrThrow({ id: user.id });
|
return this.findUserOrThrow({ id: user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
|
||||||
async createAppleUser(appleId: string, email: string) {
|
async createAppleUser(appleId: string, email: string) {
|
||||||
this.logger.log(`Creating apple user with appleId ${appleId} and email ${email}`);
|
this.logger.log(`Creating apple user with appleId ${appleId} and email ${email}`);
|
||||||
const user = await this.userRepository.createUser({
|
const user = await this.userRepository.createUser({
|
||||||
@ -109,18 +170,24 @@ export class UserService {
|
|||||||
isEmailVerified: true,
|
isEmailVerified: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.customerService.createGuardianCustomer(user.id);
|
|
||||||
|
|
||||||
return this.findUserOrThrow({ id: user.id });
|
return this.findUserOrThrow({ id: user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
async setCheckerPassword(data: SetInternalPasswordRequestDto) {
|
||||||
async verifyUserAndCreateCustomer(userId: string) {
|
const userId = await this.userTokenService.validateToken(data.token, UserType.CHECKER);
|
||||||
this.logger.log(`Verifying user ${userId} and creating customer`);
|
this.logger.log(`Setting password for checker ${userId}`);
|
||||||
await this.userRepository.update(userId, { isPhoneVerified: true });
|
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||||
await this.customerService.createGuardianCustomer(userId);
|
const hashedPasscode = bcrypt.hashSync(data.password, salt);
|
||||||
|
|
||||||
this.logger.log(`User ${userId} verified and customer created successfully`);
|
return this.userRepository.update(userId, { password: hashedPasscode, salt, isProfileCompleted: true });
|
||||||
return this.findUserOrThrow({ id: userId });
|
}
|
||||||
|
|
||||||
|
private sendCheckerAccountCreatedEmail(email: string, token: string) {
|
||||||
|
return this.notificationsService.sendEmailAsync({
|
||||||
|
to: email,
|
||||||
|
template: 'user-invite',
|
||||||
|
subject: 'Checker Account Created',
|
||||||
|
data: { inviteLink: `${this.adminPortalUrl}?token=${token}` },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { forwardRef, Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CustomerModule } from '~/customer/customer.module';
|
import { NotificationModule } from '~/common/modules/notification/notification.module';
|
||||||
import { Device, User } from './entities';
|
import { AdminUserController, UserController } from './controllers';
|
||||||
import { DeviceRepository, UserRepository } from './repositories';
|
import { Device, User, UserRegistrationToken } from './entities';
|
||||||
import { DeviceService, UserService } from './services';
|
import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories';
|
||||||
|
import { DeviceService, UserService, UserTokenService } from './services';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, Device]), forwardRef(() => CustomerModule)],
|
imports: [TypeOrmModule.forFeature([User, Device, UserRegistrationToken]), forwardRef(() => NotificationModule)],
|
||||||
providers: [UserService, DeviceService, UserRepository, DeviceRepository],
|
providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService],
|
||||||
exports: [UserService, DeviceService],
|
exports: [UserService, DeviceService, UserTokenService],
|
||||||
|
controllers: [UserController, AdminUserController],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
Reference in New Issue
Block a user