mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 16:44:54 +00:00
Compare commits
17 Commits
feat/tasks
...
feat/money
| Author | SHA1 | Date | |
|---|---|---|---|
| ed57ce6e91 | |||
| 33453b193f | |||
| b0972f1a0a | |||
| 7437403756 | |||
| 4d2f6f57f4 | |||
| 24d990592d | |||
| 5b7b7ff689 | |||
| 6fccacd085 | |||
| 51fa61dbc6 | |||
| 4867a5f858 | |||
| 687b6a5c6d | |||
| e6ed1772f7 | |||
| 1f0a14fee4 | |||
| eb70828ae0 | |||
| 220a03cc46 | |||
| 39b1e76bb5 | |||
| 83fc634d25 |
5928
package-lock.json
generated
5928
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@abdalhamid/hello": "^2.0.0",
|
||||
"@hamid/hello": "file:../libraries/test-package",
|
||||
"@keyv/redis": "^4.0.2",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/axios": "^3.1.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
@ -45,6 +46,7 @@
|
||||
"amqp-connection-manager": "^4.1.14",
|
||||
"amqplib": "^0.10.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cacheable": "^1.8.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"google-libphonenumber": "^3.2.39",
|
||||
@ -62,6 +64,7 @@
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
@ -81,6 +84,7 @@
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
@ -89,8 +93,10 @@
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"i": "^0.3.7",
|
||||
"jest": "^29.5.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"npm": "^10.9.2",
|
||||
"prettier": "^2.8.8",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
|
||||
@ -7,6 +7,7 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { addTransactionalDataSource } from 'typeorm-transactional';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { CacheModule } from './common/modules/cache/cache.module';
|
||||
import { LookupModule } from './common/modules/lookup/lookup.module';
|
||||
import { OtpModule } from './common/modules/otp/otp.module';
|
||||
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
||||
@ -19,6 +20,8 @@ import { DocumentModule } from './document/document.module';
|
||||
import { GuardianModule } from './guardian/guardian.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { JuniorModule } from './junior/junior.module';
|
||||
import { MoneyRequestModule } from './money-request/money-request.module';
|
||||
import { SavingGoalsModule } from './saving-goals/saving-goals.module';
|
||||
import { TaskModule } from './task/task.module';
|
||||
@Module({
|
||||
controllers: [],
|
||||
@ -45,12 +48,17 @@ import { TaskModule } from './task/task.module';
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
I18nModule.forRoot(buildI18nOptions()),
|
||||
CacheModule,
|
||||
|
||||
// App modules
|
||||
AuthModule,
|
||||
CustomerModule,
|
||||
JuniorModule,
|
||||
TaskModule,
|
||||
GuardianModule,
|
||||
SavingGoalsModule,
|
||||
MoneyRequestModule,
|
||||
|
||||
OtpModule,
|
||||
DocumentModule,
|
||||
LookupModule,
|
||||
|
||||
@ -2,8 +2,9 @@ import { forwardRef, Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { JuniorModule } from '~/junior/junior.module';
|
||||
import { AuthController } from './controllers';
|
||||
import { Device, User, UserNotificationSettings } from './entities';
|
||||
import { Device, User } from './entities';
|
||||
import { DeviceRepository, UserRepository } from './repositories';
|
||||
import { AuthService, DeviceService } from './services';
|
||||
import { UserService } from './services/user.service';
|
||||
@ -11,9 +12,10 @@ import { AccessTokenStrategy } from './strategies';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, UserNotificationSettings, Device]),
|
||||
TypeOrmModule.forFeature([User, Device]),
|
||||
JwtModule.register({}),
|
||||
forwardRef(() => CustomerModule),
|
||||
forwardRef(() => JuniorModule),
|
||||
],
|
||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||
controllers: [AuthController],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import {
|
||||
@ -12,6 +12,7 @@ import {
|
||||
LoginRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SetEmailRequestDto,
|
||||
setJuniorPasswordRequestDto,
|
||||
SetPasscodeRequestDto,
|
||||
VerifyUserRequestDto,
|
||||
} from '../dtos/request';
|
||||
@ -77,6 +78,13 @@ export class AuthController {
|
||||
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
||||
}
|
||||
|
||||
@Post('junior/set-passcode')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Public()
|
||||
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
|
||||
return this.authService.setJuniorPasscode(setPasscodeDto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
||||
const [res, user] = await this.authService.login(loginDto, deviceId);
|
||||
|
||||
@ -5,5 +5,6 @@ export * from './forget-password.request.dto';
|
||||
export * from './login.request.dto';
|
||||
export * from './send-forget-password-otp.request.dto';
|
||||
export * from './set-email.request.dto';
|
||||
export * from './set-junior-password.request.dto';
|
||||
export * from './set-passcode.request.dto';
|
||||
export * from './verify-user.request.dto';
|
||||
|
||||
10
src/auth/dtos/request/set-junior-password.request.dto.ts
Normal file
10
src/auth/dtos/request/set-junior-password.request.dto.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { SetPasscodeRequestDto } from './set-passcode.request.dto';
|
||||
export class setJuniorPasswordRequestDto extends SetPasscodeRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.qrToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.qrToken' }) })
|
||||
qrToken!: string;
|
||||
}
|
||||
@ -1,3 +1,2 @@
|
||||
export * from './device.entity';
|
||||
export * from './user-notification-settings.entity';
|
||||
export * from './user.entity';
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
@ -11,10 +10,8 @@ import {
|
||||
} from 'typeorm';
|
||||
import { Otp } from '~/common/modules/otp/entities';
|
||||
import { Customer } from '~/customer/entities/customer.entity';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Roles } from '../enums';
|
||||
import { Device } from './device.entity';
|
||||
import { UserNotificationSettings } from './user-notification-settings.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User extends BaseEntity {
|
||||
@ -48,22 +45,9 @@ export class User extends BaseEntity {
|
||||
@Column('text', { nullable: true, array: true, name: 'roles' })
|
||||
roles!: Roles[];
|
||||
|
||||
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
||||
profilePictureId!: string;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.user, { cascade: true, nullable: true })
|
||||
@JoinColumn({ name: 'profile_picture_id' })
|
||||
profilePicture!: Document;
|
||||
|
||||
@OneToMany(() => Otp, (otp) => otp.user)
|
||||
otp!: Otp[];
|
||||
|
||||
@OneToOne(() => UserNotificationSettings, (notificationSettings) => notificationSettings.user, {
|
||||
cascade: true,
|
||||
eager: true,
|
||||
})
|
||||
notificationSettings!: UserNotificationSettings;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
|
||||
customer!: Customer;
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
|
||||
import { User, UserNotificationSettings } from '../entities';
|
||||
import { User } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
@ -14,7 +13,6 @@ export class UserRepository {
|
||||
phoneNumber: data.phoneNumber,
|
||||
countryCode: data.countryCode,
|
||||
roles: data.roles,
|
||||
notificationSettings: UserNotificationSettings.create(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -23,11 +21,6 @@ export class UserRepository {
|
||||
return this.userRepository.findOne({ where });
|
||||
}
|
||||
|
||||
updateNotificationSettings(user: User, body: UpdateNotificationsSettingsRequestDto) {
|
||||
user.notificationSettings = UserNotificationSettings.create({ ...user.notificationSettings, ...body });
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
update(userId: string, data: Partial<User>) {
|
||||
return this.userRepository.update(userId, data);
|
||||
}
|
||||
@ -35,7 +28,6 @@ export class UserRepository {
|
||||
createUser(data: Partial<User>) {
|
||||
const user = this.userRepository.create({
|
||||
...data,
|
||||
notificationSettings: UserNotificationSettings.create(),
|
||||
});
|
||||
|
||||
return this.userRepository.save(user);
|
||||
|
||||
@ -4,6 +4,7 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||
import { OtpService } from '~/common/modules/otp/services';
|
||||
import { JuniorTokenService } from '~/junior/services';
|
||||
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
||||
import {
|
||||
CreateUnverifiedUserRequestDto,
|
||||
@ -13,6 +14,7 @@ import {
|
||||
LoginRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SetEmailRequestDto,
|
||||
setJuniorPasswordRequestDto,
|
||||
} from '../dtos/request';
|
||||
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
||||
import { User } from '../entities';
|
||||
@ -32,6 +34,7 @@ export class AuthService {
|
||||
private readonly configService: ConfigService,
|
||||
private readonly userService: UserService,
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly juniorTokenService: JuniorTokenService,
|
||||
) {}
|
||||
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
||||
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
||||
@ -186,6 +189,14 @@ export class AuthService {
|
||||
return [tokens, user];
|
||||
}
|
||||
|
||||
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
||||
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
|
||||
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
|
||||
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
|
||||
await this.juniorTokenService.invalidateToken(body.qrToken);
|
||||
}
|
||||
|
||||
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
|
||||
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
|
||||
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
|
||||
@ -15,15 +15,6 @@ export class UserService {
|
||||
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
|
||||
) {}
|
||||
|
||||
async updateNotificationSettings(userId: string, body: UpdateNotificationsSettingsRequestDto) {
|
||||
const user = await this.findUserOrThrow({ id: userId });
|
||||
|
||||
const notificationSettings = (await this.userRepository.updateNotificationSettings(user, body))
|
||||
.notificationSettings;
|
||||
|
||||
return notificationSettings;
|
||||
}
|
||||
|
||||
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
return this.userRepository.findOne(where);
|
||||
}
|
||||
@ -74,6 +65,7 @@ export class UserService {
|
||||
await this.customerService.createCustomer(
|
||||
{
|
||||
guardian: Guardian.create({ id: user.id }),
|
||||
notificationSettings: new CustomerNotificationSettings(),
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
17
src/common/modules/cache/cache.module.ts
vendored
Normal file
17
src/common/modules/cache/cache.module.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { buildKeyvOptions } from '~/core/module-options';
|
||||
import { CacheService } from './services';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'CACHE_INSTANCE',
|
||||
useFactory: (config: ConfigService) => buildKeyvOptions(config),
|
||||
inject: [ConfigService],
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
exports: ['CACHE_INSTANCE', CacheService],
|
||||
})
|
||||
export class CacheModule {}
|
||||
19
src/common/modules/cache/services/cache.services.ts
vendored
Normal file
19
src/common/modules/cache/services/cache.services.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cacheable } from 'cacheable';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
constructor(@Inject('CACHE_INSTANCE') private readonly cache: Cacheable) {}
|
||||
|
||||
get<T>(key: string): Promise<T | undefined> {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number | string): Promise<void> {
|
||||
await this.cache.set(key, value, ttl);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
1
src/common/modules/cache/services/index.ts
vendored
Normal file
1
src/common/modules/cache/services/index.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from './cache.services';
|
||||
@ -20,4 +20,13 @@ export class LookupController {
|
||||
|
||||
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
|
||||
}
|
||||
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@Get('default-task-logos')
|
||||
@ApiDataArrayResponse(DocumentMetaResponseDto)
|
||||
async findDefaultTaskLogos() {
|
||||
const avatars = await this.lookupService.findDefaultTasksLogo();
|
||||
|
||||
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DocumentType } from '~/document/enums';
|
||||
import { DocumentService } from '~/document/services';
|
||||
import { DocumentService, OciService } from '~/document/services';
|
||||
|
||||
@Injectable()
|
||||
export class LookupService {
|
||||
constructor(private readonly documentService: DocumentService) {}
|
||||
findDefaultAvatar() {
|
||||
return this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
|
||||
constructor(private readonly documentService: DocumentService, private readonly ociService: OciService) {}
|
||||
async findDefaultAvatar() {
|
||||
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
|
||||
|
||||
await Promise.all(
|
||||
documents.map(async (document) => {
|
||||
document.url = await this.ociService.generatePreSignedUrl(document);
|
||||
}),
|
||||
);
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
async findDefaultTasksLogo() {
|
||||
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_TASKS_LOGO });
|
||||
|
||||
await Promise.all(
|
||||
documents.map(async (document) => {
|
||||
document.url = await this.ociService.generatePreSignedUrl(document);
|
||||
}),
|
||||
);
|
||||
|
||||
return documents;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from '././keyv-options';
|
||||
export * from './config-options';
|
||||
export * from './typeorm-options';
|
||||
export * from './logger-options';
|
||||
export * from './typeorm-options';
|
||||
|
||||
8
src/core/module-options/keyv-options.ts
Normal file
8
src/core/module-options/keyv-options.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cacheable } from 'cacheable';
|
||||
|
||||
export function buildKeyvOptions(config: ConfigService) {
|
||||
const secondary = new KeyvRedis(config.get('REDIS_URL'));
|
||||
return new Cacheable({ secondary, ttl: config.get('REDIS_TTL') });
|
||||
}
|
||||
1
src/core/types/index.ts
Normal file
1
src/core/types/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './object-values.type';
|
||||
13
src/core/types/object-values.type.ts
Normal file
13
src/core/types/object-values.type.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Converts an object's values to a union type.
|
||||
* Should be used in conjunction with constant objects to define enums.
|
||||
*
|
||||
* @example
|
||||
* const Status = { SUCCEEDED: 'Success', FAILED: 'Failure' } as const;
|
||||
* type Status = ObjectValues<typeof Status>; // 'Success' | 'Failure'
|
||||
* const foo: Status = Status.SUCCEEDED;
|
||||
* const bar: Status = 'Failure';
|
||||
*
|
||||
* @see https://youtu.be/jjMbPt_H3RQ
|
||||
*/
|
||||
export type ObjectValues<T> = T[keyof T];
|
||||
@ -1,9 +1,9 @@
|
||||
import { Body, Controller, Patch, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ApiDataResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
|
||||
@ -15,9 +15,18 @@ import { CustomerService } from '../services';
|
||||
export class CustomerController {
|
||||
constructor(private readonly customerService: CustomerService) {}
|
||||
|
||||
@Get('/profile')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiDataResponse(CustomerResponseDto)
|
||||
async getCustomerProfile(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
const customer = await this.customerService.findCustomerById(sub);
|
||||
|
||||
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||
}
|
||||
|
||||
@Patch('')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiDataResponse(CustomerResponseDto)
|
||||
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
|
||||
const customer = await this.customerService.updateCustomer(sub, body);
|
||||
|
||||
@ -26,6 +35,7 @@ export class CustomerController {
|
||||
|
||||
@Patch('settings/notifications')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiDataResponse(NotificationSettingsResponseDto)
|
||||
async updateNotificationSettings(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Body() body: UpdateNotificationsSettingsRequestDto,
|
||||
|
||||
@ -3,11 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '~/auth/auth.module';
|
||||
import { CustomerController } from './controllers';
|
||||
import { Customer } from './entities';
|
||||
import { CustomerNotificationSettings } from './entities/customer-notification-settings.entity';
|
||||
import { CustomerRepository } from './repositories/customer.repository';
|
||||
import { CustomerService } from './services';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Customer]), forwardRef(() => AuthModule)],
|
||||
imports: [TypeOrmModule.forFeature([Customer, CustomerNotificationSettings]), forwardRef(() => AuthModule)],
|
||||
controllers: [CustomerController],
|
||||
providers: [CustomerService, CustomerRepository],
|
||||
exports: [CustomerService],
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDateString, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { IsAbove18 } from '~/core/decorators/validations';
|
||||
export class UpdateCustomerRequestDto {
|
||||
@ -26,4 +26,8 @@ export class UpdateCustomerRequestDto {
|
||||
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||
@IsOptional()
|
||||
dateOfBirth!: Date;
|
||||
|
||||
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) })
|
||||
profilePictureId!: string;
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { NotificationSettingsResponseDto } from './notification-settings.response.dto';
|
||||
|
||||
export class CustomerResponseDto {
|
||||
@ApiProperty()
|
||||
@ -50,6 +52,12 @@ export class CustomerResponseDto {
|
||||
@ApiProperty()
|
||||
isGuardian!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
notificationSettings!: NotificationSettingsResponseDto;
|
||||
|
||||
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
|
||||
profilePicture!: DocumentMetaResponseDto | null;
|
||||
|
||||
constructor(customer: Customer) {
|
||||
this.id = customer.id;
|
||||
this.customerStatus = customer.customerStatus;
|
||||
@ -67,5 +75,7 @@ export class CustomerResponseDto {
|
||||
this.gender = customer.gender;
|
||||
this.isJunior = customer.isJunior;
|
||||
this.isGuardian = customer.isGuardian;
|
||||
this.notificationSettings = new NotificationSettingsResponseDto(customer.notificationSettings);
|
||||
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UserNotificationSettings } from '~/auth/entities';
|
||||
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
||||
|
||||
export class NotificationSettingsResponseDto {
|
||||
@ApiProperty()
|
||||
@ -11,7 +11,7 @@ export class NotificationSettingsResponseDto {
|
||||
@ApiProperty()
|
||||
isSmsEnabled!: boolean;
|
||||
|
||||
constructor(notificationSettings: UserNotificationSettings) {
|
||||
constructor(notificationSettings: CustomerNotificationSettings) {
|
||||
this.isEmailEnabled = notificationSettings.isEmailEnabled;
|
||||
this.isPushEnabled = notificationSettings.isPushEnabled;
|
||||
this.isSmsEnabled = notificationSettings.isSmsEnabled;
|
||||
|
||||
@ -8,10 +8,10 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { Customer } from '~/customer/entities';
|
||||
|
||||
@Entity('user_notification_settings')
|
||||
export class UserNotificationSettings extends BaseEntity {
|
||||
@Entity('cutsomer_notification_settings')
|
||||
export class CustomerNotificationSettings extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@ -24,9 +24,9 @@ export class UserNotificationSettings extends BaseEntity {
|
||||
@Column({ name: 'is_sms_enabled', default: false })
|
||||
isSmsEnabled!: boolean;
|
||||
|
||||
@OneToOne(() => User, (user) => user.notificationSettings, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
@OneToOne(() => Customer, (customer) => customer.notificationSettings, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'customer_id' })
|
||||
customer!: Customer;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||
createdAt!: Date;
|
||||
@ -9,10 +9,12 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { CustomerNotificationSettings } from './customer-notification-settings.entity';
|
||||
|
||||
@Entity()
|
||||
@Entity('customers')
|
||||
export class Customer extends BaseEntity {
|
||||
@PrimaryColumn('uuid')
|
||||
id!: string;
|
||||
@ -65,6 +67,19 @@ export class Customer extends BaseEntity {
|
||||
@Column('varchar', { name: 'user_id' })
|
||||
userId!: string;
|
||||
|
||||
@Column('varchar', { name: 'profile_picture_id', nullable: true })
|
||||
profilePictureId!: string;
|
||||
|
||||
@OneToOne(() => CustomerNotificationSettings, (notificationSettings) => notificationSettings.customer, {
|
||||
cascade: true,
|
||||
eager: true,
|
||||
})
|
||||
notificationSettings!: CustomerNotificationSettings;
|
||||
|
||||
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
|
||||
@JoinColumn({ name: 'profile_picture_id' })
|
||||
profilePicture!: Document;
|
||||
|
||||
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
|
||||
@ -3,19 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { UpdateCustomerRequestDto } from '../dtos/request';
|
||||
import { UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { Customer } from '../entities';
|
||||
import { CustomerNotificationSettings } from '../entities/customer-notification-settings.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerRepository {
|
||||
constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {}
|
||||
|
||||
updateCustomer(id: string, data: UpdateCustomerRequestDto) {
|
||||
updateCustomer(id: string, data: Partial<Customer>) {
|
||||
return this.customerRepository.update(id, data);
|
||||
}
|
||||
|
||||
findOne(where: FindOptionsWhere<Customer>) {
|
||||
return this.customerRepository.findOne({ where });
|
||||
return this.customerRepository.findOne({ where, relations: ['profilePicture'] });
|
||||
}
|
||||
|
||||
createCustomer(customerData: Partial<Customer>, user: User) {
|
||||
@ -29,4 +30,12 @@ export class CustomerRepository {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
updateNotificationSettings(customer: Customer, body: UpdateNotificationsSettingsRequestDto) {
|
||||
customer.notificationSettings = CustomerNotificationSettings.create({
|
||||
...customer.notificationSettings,
|
||||
...body,
|
||||
});
|
||||
return this.customerRepository.save(customer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { User } from '~/auth/entities';
|
||||
import { UserService } from '~/auth/services/user.service';
|
||||
import { OciService } from '~/document/services';
|
||||
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
|
||||
import { Customer } from '../entities';
|
||||
import { CustomerRepository } from '../repositories/customer.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerService {
|
||||
constructor(
|
||||
@Inject(forwardRef(() => UserService)) private readonly userService: UserService,
|
||||
private readonly customerRepository: CustomerRepository,
|
||||
) {}
|
||||
updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
|
||||
return this.userService.updateNotificationSettings(userId, data);
|
||||
constructor(private readonly customerRepository: CustomerRepository, private readonly ociService: OciService) {}
|
||||
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
|
||||
const customer = await this.findCustomerById(userId);
|
||||
|
||||
const notificationSettings = (await this.customerRepository.updateNotificationSettings(customer, data))
|
||||
.notificationSettings;
|
||||
|
||||
return notificationSettings;
|
||||
}
|
||||
|
||||
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
|
||||
@ -26,9 +28,14 @@ export class CustomerService {
|
||||
|
||||
async findCustomerById(id: string) {
|
||||
const customer = await this.customerRepository.findOne({ id });
|
||||
|
||||
if (!customer) {
|
||||
throw new BadRequestException('CUSTOMER.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (customer.profilePicture) {
|
||||
customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture);
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export class CreateDocumentEntity1732434281561 implements MigrationInterface {
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"extension" character varying(255) NOT NULL,
|
||||
"documentType" character varying(255) NOT NULL,
|
||||
"document_type" character varying(255) NOT NULL,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
|
||||
|
||||
@ -16,16 +16,10 @@ export class CreateUserEntity1733206728721 implements MigrationInterface {
|
||||
"apple_id" character varying(255),
|
||||
"is_profile_completed" boolean NOT NULL DEFAULT false,
|
||||
"roles" text array,
|
||||
"profile_picture_id" uuid,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
|
||||
CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateNotificationSettingsTable1733231692252 implements MigrationInterface {
|
||||
name = 'CreateNotificationSettingsTable1733231692252';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_notification_settings"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"is_email_enabled" boolean NOT NULL DEFAULT false,
|
||||
"is_push_enabled" boolean NOT NULL DEFAULT false,
|
||||
"is_sms_enabled" boolean NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"user_id" uuid, CONSTRAINT "REL_52182ffd0f785e8256f8fcb4fd" UNIQUE ("user_id"),
|
||||
CONSTRAINT "PK_a195de67d093e096152f387afbd" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_notification_settings" ADD CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_notification_settings" DROP CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_notification_settings"`);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "customer"
|
||||
`CREATE TABLE "customers"
|
||||
("id" uuid NOT NULL,
|
||||
"customer_status" character varying(255) NOT NULL DEFAULT 'PENDING',
|
||||
"rejection_reason" text,
|
||||
@ -22,19 +22,25 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
|
||||
"gender" character varying(255),
|
||||
"is_junior" boolean NOT NULL DEFAULT false,
|
||||
"is_guardian" boolean NOT NULL DEFAULT false,
|
||||
"profile_picture_id" uuid,
|
||||
"user_id" uuid NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
|
||||
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
|
||||
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
|
||||
CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customer" ADD CONSTRAINT "FK_5d1f609371a285123294fddcf3a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customers" ADD CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "customer" DROP CONSTRAINT "FK_5d1f609371a285123294fddcf3a"`);
|
||||
await queryRunner.query(`DROP TABLE "customer"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661"`);
|
||||
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
|
||||
await queryRunner.query(`DROP TABLE "customers"`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ export class CreateJuniorEntity1733731507261 implements MigrationInterface {
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_4662c4433223c01fe69fc1382f5" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ export class CreateGuardianEntity1733732021622 implements MigrationInterface {
|
||||
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
41
src/db/migrations/1733990253208-seeds-default-tasks-logo.ts
Normal file
41
src/db/migrations/1733990253208-seeds-default-tasks-logo.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Document } from '~/document/entities';
|
||||
import { DocumentType } from '~/document/enums';
|
||||
const DEFAULT_TASK_LOGOS = [
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'bed-furniture',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'dog',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'dish-washing',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
name: 'walking-the-dog',
|
||||
extension: '.jpg',
|
||||
documentType: DocumentType.DEFAULT_TASKS_LOGO,
|
||||
},
|
||||
];
|
||||
export class SeedsDefaultTasksLogo1733990253208 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.manager.getRepository(Document).save(DEFAULT_TASK_LOGOS);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await DEFAULT_TASK_LOGOS.forEach(async (logo) => {
|
||||
await queryRunner.manager.getRepository(Document).delete({ name: logo.name, documentType: logo.documentType });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateCustomerNotificationsSettingsTable1733993920226 implements MigrationInterface {
|
||||
name = 'CreateCustomerNotificationsSettingsTable1733993920226';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "cutsomer_notification_settings"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"is_email_enabled" boolean NOT NULL DEFAULT false,
|
||||
"is_push_enabled" boolean NOT NULL DEFAULT false,
|
||||
"is_sms_enabled" boolean NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"customer_id" uuid, CONSTRAINT "REL_32f2b707407298a9eecd6cc7ea" UNIQUE ("customer_id"),
|
||||
CONSTRAINT "PK_ea94fb22410c89ae6b37d63b0e3" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "cutsomer_notification_settings" ADD CONSTRAINT "FK_32f2b707407298a9eecd6cc7ea6" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "cutsomer_notification_settings" DROP CONSTRAINT "FK_32f2b707407298a9eecd6cc7ea6"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "cutsomer_notification_settings"`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateSavingGoalsEntities1734246386471 implements MigrationInterface {
|
||||
name = 'CreateSavingGoalsEntities1734246386471';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "saving_goals"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"description" character varying(255),
|
||||
"due_date" date NOT NULL,
|
||||
"target_amount" numeric(12,3) NOT NULL,
|
||||
"current_amount" numeric(12,3) NOT NULL DEFAULT '0',
|
||||
"image_id" uuid, "junior_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_5193f14c1c3a38e6657a159795e" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "categories"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"type" character varying(255) NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"junior_id" uuid, CONSTRAINT "PK_24dbc6126a28ff948da33e97d3b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "saving_goals_categories"
|
||||
("saving_goal_id" uuid NOT NULL,
|
||||
"category_id" uuid NOT NULL,
|
||||
CONSTRAINT "PK_a49d4f57d06d0a36a8385b6c28f" PRIMARY KEY ("saving_goal_id", "category_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_d421de423f21c01672ea7c2e98" ON "saving_goals_categories" ("saving_goal_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b0a721a8f7f5b6fe93f3603ebc" ON "saving_goals_categories" ("category_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals" ADD CONSTRAINT "FK_dad35932272342c1a247a2cee1c" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals" ADD CONSTRAINT "FK_f494ba0a361b2f9cbe5f6e56e38" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "categories" ADD CONSTRAINT "FK_4f98e0b010d5e90cae0a2007748" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals_categories" ADD CONSTRAINT "FK_d421de423f21c01672ea7c2e98f" FOREIGN KEY ("saving_goal_id") REFERENCES "saving_goals"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "saving_goals_categories" ADD CONSTRAINT "FK_b0a721a8f7f5b6fe93f3603ebc8" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals_categories" DROP CONSTRAINT "FK_b0a721a8f7f5b6fe93f3603ebc8"`);
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals_categories" DROP CONSTRAINT "FK_d421de423f21c01672ea7c2e98f"`);
|
||||
await queryRunner.query(`ALTER TABLE "categories" DROP CONSTRAINT "FK_4f98e0b010d5e90cae0a2007748"`);
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals" DROP CONSTRAINT "FK_f494ba0a361b2f9cbe5f6e56e38"`);
|
||||
await queryRunner.query(`ALTER TABLE "saving_goals" DROP CONSTRAINT "FK_dad35932272342c1a247a2cee1c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b0a721a8f7f5b6fe93f3603ebc"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d421de423f21c01672ea7c2e98"`);
|
||||
await queryRunner.query(`DROP TABLE "saving_goals_categories"`);
|
||||
await queryRunner.query(`DROP TABLE "categories"`);
|
||||
await queryRunner.query(`DROP TABLE "saving_goals"`);
|
||||
}
|
||||
}
|
||||
57
src/db/migrations/1734247702310-seeds-goals-categories.ts
Normal file
57
src/db/migrations/1734247702310-seeds-goals-categories.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { CategoryType } from '~/saving-goals/enums';
|
||||
const DEFAULT_CATEGORIES = [
|
||||
{
|
||||
name: 'School',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Toys',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Games',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Clothes',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Hobbies',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Party',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Sport',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'University',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
{
|
||||
name: 'Travel',
|
||||
type: CategoryType.GLOBAL,
|
||||
},
|
||||
];
|
||||
export class SeedsGoalsCategories1734247702310 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`INSERT INTO categories (name, type) VALUES ${DEFAULT_CATEGORIES.map(
|
||||
(category) => `('${category.name}', '${category.type}')`,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM categories WHERE name IN (${DEFAULT_CATEGORIES.map(
|
||||
(category) => `'${category.name}'`,
|
||||
)}) AND type = '${CategoryType.GLOBAL}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateJuniorRegistrationTokenTable1734262619426 implements MigrationInterface {
|
||||
name = 'CreateJuniorRegistrationTokenTable1734262619426';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "junior_registration_tokens"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"token" character varying(255) NOT NULL,
|
||||
"is_used" boolean NOT NULL DEFAULT false,
|
||||
"expiry_date" TIMESTAMP NOT NULL,
|
||||
"junior_id" uuid NOT NULL,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "UQ_e6a3e23d5a63be76812dc5d7728" UNIQUE ("token"),
|
||||
CONSTRAINT "PK_610992ebec8f664113ae48b946f" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_e6a3e23d5a63be76812dc5d772" ON "junior_registration_tokens" ("token") `);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "junior_registration_tokens" ADD CONSTRAINT "FK_19c52317b5b7aeecc0a11789b53" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "junior_registration_tokens" DROP CONSTRAINT "FK_19c52317b5b7aeecc0a11789b53"`,
|
||||
);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_e6a3e23d5a63be76812dc5d772"`);
|
||||
await queryRunner.query(`DROP TABLE "junior_registration_tokens"`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateMoneyRequestEntity1734503895302 implements MigrationInterface {
|
||||
name = 'CreateMoneyRequestEntity1734503895302';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "money_requests"
|
||||
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"requested_amount" numeric(10,3) NOT NULL,
|
||||
"message" character varying NOT NULL,
|
||||
"frequency" character varying NOT NULL DEFAULT 'ONE_TIME',
|
||||
"status" character varying NOT NULL DEFAULT 'PENDING',
|
||||
"reviewed_at" TIMESTAMP WITH TIME ZONE,
|
||||
"end_date" TIMESTAMP WITH TIME ZONE,
|
||||
"requester_id" uuid NOT NULL,
|
||||
"reviewer_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_28cff23e9fb06cd5dbf73cd53e7" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_5cce02836c6033b6e2412995e34" FOREIGN KEY ("requester_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_75ba0766db9a7bf03126facf31c" FOREIGN KEY ("reviewer_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_75ba0766db9a7bf03126facf31c"`);
|
||||
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_5cce02836c6033b6e2412995e34"`);
|
||||
await queryRunner.query(`DROP TABLE "money_requests"`);
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,16 @@
|
||||
export * from './1732434281561-create-document-entity';
|
||||
export * from './1733206728721-create-user-entity';
|
||||
export * from './1733209041336-create-otp-entity';
|
||||
export * from './1733231692252-create-notification-settings-table';
|
||||
export * from './1733298524771-create-customer-entity';
|
||||
export * from './1733314952318-create-device-entity';
|
||||
export * from './1733731507261-create-junior-entity';
|
||||
export * from './1733732021622-create-guardian-entity';
|
||||
export * from './1733748083604-create-theme-entity';
|
||||
export * from './1733750228289-seed-default-avatar';
|
||||
export * from './1733904556416-create-task-entities';
|
||||
export * from './1733990253208-seeds-default-tasks-logo';
|
||||
export * from './1733993920226-create-customer-notifications-settings-table';
|
||||
export * from './1734246386471-create-saving-goals-entities';
|
||||
export * from './1734247702310-seeds-goals-categories';
|
||||
export * from './1734262619426-create-junior-registration-token-table';
|
||||
export * from './1734503895302-create-money-request-entity';
|
||||
|
||||
@ -4,4 +4,8 @@ export const BUCKETS: Record<DocumentType, string> = {
|
||||
[DocumentType.PROFILE_PICTURE]: 'profile-pictures',
|
||||
[DocumentType.PASSPORT]: 'passports',
|
||||
[DocumentType.DEFAULT_AVATAR]: 'avatars',
|
||||
[DocumentType.DEFAULT_TASKS_LOGO]: 'tasks-logo',
|
||||
[DocumentType.CUSTOM_AVATAR]: 'avatars',
|
||||
[DocumentType.CUSTOM_TASKS_LOGO]: 'tasks-logo',
|
||||
[DocumentType.GOALS]: 'goals',
|
||||
};
|
||||
|
||||
@ -24,7 +24,9 @@ export class DocumentController {
|
||||
},
|
||||
documentType: {
|
||||
type: 'string',
|
||||
enum: Object.values(DocumentType),
|
||||
enum: Object.values(DocumentType).filter(
|
||||
(value) => ![DocumentType.DEFAULT_AVATAR, DocumentType.DEFAULT_TASKS_LOGO].includes(value),
|
||||
),
|
||||
},
|
||||
},
|
||||
required: ['document', 'documentType'],
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CacheModule } from '~/common/modules/cache/cache.module';
|
||||
import { DocumentController } from './controllers';
|
||||
import { Document } from './entities';
|
||||
import { DocumentRepository } from './repositories';
|
||||
import { DocumentService, OciService } from './services';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Document])],
|
||||
imports: [TypeOrmModule.forFeature([Document]), CacheModule],
|
||||
controllers: [DocumentController],
|
||||
providers: [DocumentService, OciService, DocumentRepository],
|
||||
exports: [DocumentService],
|
||||
exports: [DocumentService, OciService],
|
||||
})
|
||||
export class DocumentModule {}
|
||||
|
||||
@ -15,18 +15,14 @@ export class DocumentMetaResponseDto {
|
||||
@ApiProperty()
|
||||
documentType!: DocumentType;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt!: Date;
|
||||
@ApiProperty({ type: String })
|
||||
url!: string | null;
|
||||
|
||||
constructor(document: Document) {
|
||||
this.id = document.id;
|
||||
this.name = document.name;
|
||||
this.extension = document.extension;
|
||||
this.documentType = document.documentType;
|
||||
this.createdAt = document.createdAt;
|
||||
this.updatedAt = document.updatedAt;
|
||||
this.url = document.url || null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Junior, Theme } from '~/junior/entities';
|
||||
import { SavingGoal } from '~/saving-goals/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
|
||||
import { DocumentType } from '../enums';
|
||||
@ -16,16 +18,16 @@ export class Document {
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
extension!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
@Column({ type: 'varchar', length: 255, name: 'document_type' })
|
||||
documentType!: DocumentType;
|
||||
|
||||
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'CASCADE' })
|
||||
user?: User;
|
||||
@OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
|
||||
customerPicture?: Customer;
|
||||
|
||||
@OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'CASCADE' })
|
||||
@OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'SET NULL' })
|
||||
juniorCivilIdFront?: User;
|
||||
|
||||
@OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'CASCADE' })
|
||||
@OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'SET NULL' })
|
||||
juniorCivilIdBack?: User;
|
||||
|
||||
@OneToMany(() => Theme, (theme) => theme.avatar)
|
||||
@ -37,9 +39,15 @@ export class Document {
|
||||
@OneToMany(() => TaskSubmission, (taskSubmission) => taskSubmission.proofOfCompletion)
|
||||
submissions?: TaskSubmission[];
|
||||
|
||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
|
||||
goals?: SavingGoal[];
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
// virtual field
|
||||
url?: string;
|
||||
}
|
||||
|
||||
@ -2,4 +2,8 @@ export enum DocumentType {
|
||||
PROFILE_PICTURE = 'PROFILE_PICTURE',
|
||||
PASSPORT = 'PASSPORT',
|
||||
DEFAULT_AVATAR = 'DEFAULT_AVATAR',
|
||||
DEFAULT_TASKS_LOGO = 'DEFAULT_TASKS_LOGO',
|
||||
CUSTOM_AVATAR = 'CUSTOM_AVATAR',
|
||||
CUSTOM_TASKS_LOGO = 'CUSTOM_TASKS_LOGO',
|
||||
GOALS = 'GOALS',
|
||||
}
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import moment from 'moment';
|
||||
import { Region, SimpleAuthenticationDetailsProvider } from 'oci-common';
|
||||
import { ObjectStorageClient } from 'oci-objectstorage';
|
||||
import { CreatePreauthenticatedRequestDetails } from 'oci-objectstorage/lib/model';
|
||||
import path from 'path';
|
||||
import { CacheService } from '~/common/modules/cache/services';
|
||||
import { BUCKETS } from '../constants';
|
||||
import { UploadDocumentRequestDto } from '../dtos/request';
|
||||
import { UploadResponseDto } from '../dtos/response';
|
||||
import { Document } from '../entities';
|
||||
import { generateNewFileName } from '../utils';
|
||||
|
||||
const TWO = 2;
|
||||
@Injectable()
|
||||
export class OciService {
|
||||
private readonly ociClient: ObjectStorageClient;
|
||||
@ -20,7 +24,7 @@ export class OciService {
|
||||
private readonly namespace: string = this.configService.getOrThrow('OCI_NAMESPACE');
|
||||
private readonly region: string = this.configService.getOrThrow('OCI_REGION');
|
||||
private readonly logger = new Logger(OciService.name);
|
||||
constructor(private configService: ConfigService) {
|
||||
constructor(private configService: ConfigService, private readonly cacheService: CacheService) {
|
||||
this.ociClient = new ObjectStorageClient({
|
||||
authenticationDetailsProvider: new SimpleAuthenticationDetailsProvider(
|
||||
this.tenancyId,
|
||||
@ -60,4 +64,40 @@ export class OciService {
|
||||
documentType,
|
||||
});
|
||||
}
|
||||
|
||||
async generatePreSignedUrl(document?: Document): Promise<string | any> {
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
const cachedUrl = await this.cacheService.get<string>(document.id);
|
||||
|
||||
if (cachedUrl) {
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
const bucketName = BUCKETS[document.documentType];
|
||||
const objectName = document.name;
|
||||
const expiration = moment().add(TWO, 'hours').toDate();
|
||||
|
||||
try {
|
||||
this.logger.debug(`Generating pre-signed url for object ${objectName} in bucket ${bucketName}`);
|
||||
const res = await this.ociClient.createPreauthenticatedRequest({
|
||||
namespaceName: this.namespace,
|
||||
bucketName,
|
||||
createPreauthenticatedRequestDetails: {
|
||||
name: objectName,
|
||||
accessType: CreatePreauthenticatedRequestDetails.AccessType.AnyObjectRead,
|
||||
timeExpires: expiration,
|
||||
objectName,
|
||||
},
|
||||
retryConfiguration: { terminationStrategy: { shouldTerminate: () => true } },
|
||||
});
|
||||
|
||||
this.cacheService.set(document.id, res.preauthenticatedRequest.fullPath + objectName, '1h');
|
||||
return res.preauthenticatedRequest.fullPath + objectName;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error generating pre-signed url: ${error}`);
|
||||
return document.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { MoneyRequest } from '~/money-request/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
|
||||
@Entity('guardians')
|
||||
@ -31,6 +32,9 @@ export class Guardian extends BaseEntity {
|
||||
@OneToMany(() => Task, (task) => task.assignedBy)
|
||||
tasks?: Task[];
|
||||
|
||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.reviewer)
|
||||
moneyRequests?: MoneyRequest[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
|
||||
@ -2,14 +2,14 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/co
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { AllowedRoles, AuthenticatedUser, Public } from '~/common/decorators';
|
||||
import { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||
import { JuniorResponseDto, ThemeResponseDto } from '../dtos/response';
|
||||
import { JuniorResponseDto, QrCodeValidationResponseDto, ThemeResponseDto } from '../dtos/response';
|
||||
import { JuniorService } from '../services';
|
||||
|
||||
@Controller('juniors')
|
||||
@ -23,9 +23,9 @@ export class JuniorController {
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(JuniorResponseDto)
|
||||
async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) {
|
||||
const junior = await this.juniorService.createJuniors(body, user.sub);
|
||||
const token = await this.juniorService.createJuniors(body, user.sub);
|
||||
|
||||
return ResponseFactory.data(new JuniorResponseDto(junior));
|
||||
return ResponseFactory.data(token);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ -53,7 +53,7 @@ export class JuniorController {
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
||||
) {
|
||||
const junior = await this.juniorService.findJuniorById(juniorId, user.sub);
|
||||
const junior = await this.juniorService.findJuniorById(juniorId, false, user.sub);
|
||||
|
||||
return ResponseFactory.data(new JuniorResponseDto(junior));
|
||||
}
|
||||
@ -66,4 +66,23 @@ export class JuniorController {
|
||||
const theme = await this.juniorService.setTheme(body, user.sub);
|
||||
return ResponseFactory.data(new ThemeResponseDto(theme));
|
||||
}
|
||||
|
||||
@Get(':juniorId/qr-code')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse('string')
|
||||
async generateQrCode(@Param('juniorId', CustomParseUUIDPipe) juniorId: string) {
|
||||
const qrCode = await this.juniorService.generateToken(juniorId);
|
||||
|
||||
return ResponseFactory.data(qrCode);
|
||||
}
|
||||
|
||||
@Get('qr-code/:token/validate')
|
||||
@Public()
|
||||
@ApiDataResponse(QrCodeValidationResponseDto)
|
||||
async validateToken(@Param('token') token: string) {
|
||||
const junior = await this.juniorService.validateToken(token);
|
||||
|
||||
return ResponseFactory.data(new QrCodeValidationResponseDto(junior));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './junior.response.dto';
|
||||
export * from './qr-code-validation-details.response.dto';
|
||||
export * from './qr-code-validation.response.dto';
|
||||
export * from './theme.response.dto';
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { Relationship } from '~/junior/enums';
|
||||
|
||||
@ -12,13 +13,15 @@ export class JuniorResponseDto {
|
||||
@ApiProperty({ example: 'relationship' })
|
||||
relationship!: Relationship;
|
||||
|
||||
@ApiProperty({ example: 'profilePictureId' })
|
||||
profilePictureId: string | null;
|
||||
@ApiProperty({ type: DocumentMetaResponseDto })
|
||||
profilePicture!: DocumentMetaResponseDto | null;
|
||||
|
||||
constructor(junior: Junior) {
|
||||
this.id = junior.id;
|
||||
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
|
||||
this.relationship = junior.relationship;
|
||||
this.profilePictureId = junior.customer.user.profilePictureId;
|
||||
this.profilePicture = junior.customer.profilePicture
|
||||
? new DocumentMetaResponseDto(junior.customer.profilePicture)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { GuardianRelationship } from '~/junior/enums';
|
||||
|
||||
export class QrCodeValidationDetailsResponse {
|
||||
@ApiProperty()
|
||||
fullname!: string;
|
||||
|
||||
@ApiProperty()
|
||||
phoneNumber!: string;
|
||||
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@ApiProperty()
|
||||
relationship?: string;
|
||||
|
||||
@ApiProperty()
|
||||
dateOfBirth!: Date;
|
||||
|
||||
constructor(junior: Junior, guardian?: Guardian) {
|
||||
const person = guardian ? guardian : junior;
|
||||
|
||||
this.fullname = `${person.customer.firstName} ${person.customer.lastName}`;
|
||||
this.phoneNumber = person.customer.user.phoneNumber;
|
||||
this.email = person.customer.user.email;
|
||||
this.dateOfBirth = person.customer.dateOfBirth;
|
||||
this.relationship = guardian ? junior.relationship : GuardianRelationship[junior.relationship];
|
||||
}
|
||||
}
|
||||
16
src/junior/dtos/response/qr-code-validation.response.dto.ts
Normal file
16
src/junior/dtos/response/qr-code-validation.response.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { QrCodeValidationDetailsResponse } from './qr-code-validation-details.response.dto';
|
||||
|
||||
export class QrCodeValidationResponseDto {
|
||||
@ApiProperty({ type: QrCodeValidationDetailsResponse })
|
||||
parentDetails: any;
|
||||
|
||||
@ApiProperty({ type: QrCodeValidationDetailsResponse })
|
||||
childDetails: any;
|
||||
|
||||
constructor(junior: Junior) {
|
||||
this.parentDetails = new QrCodeValidationDetailsResponse(junior, junior.guardian);
|
||||
this.childDetails = new QrCodeValidationDetailsResponse(junior);
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './junior-registration-token.entity';
|
||||
export * from './junior.entity';
|
||||
export * from './theme.entity';
|
||||
|
||||
41
src/junior/entities/junior-registration-token.entity.ts
Normal file
41
src/junior/entities/junior-registration-token.entity.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Junior } from './junior.entity';
|
||||
|
||||
@Entity('junior_registration_tokens')
|
||||
export class JuniorRegistrationToken extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'token', unique: true })
|
||||
@Index()
|
||||
token!: string;
|
||||
|
||||
@Column({ type: 'boolean', default: false, name: 'is_used' })
|
||||
isUsed!: boolean;
|
||||
|
||||
@Column({ type: 'timestamp', name: 'expiry_date' })
|
||||
expiryDate!: Date;
|
||||
|
||||
@Column({ type: 'uuid', name: 'junior_id' })
|
||||
juniorId!: string;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.registrationTokens)
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@ -13,8 +13,11 @@ import {
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { MoneyRequest } from '~/money-request/entities';
|
||||
import { Category, SavingGoal } from '~/saving-goals/entities';
|
||||
import { Task } from '~/task/entities';
|
||||
import { Relationship } from '../enums';
|
||||
import { JuniorRegistrationToken } from './junior-registration-token.entity';
|
||||
import { Theme } from './theme.entity';
|
||||
|
||||
@Entity('juniors')
|
||||
@ -57,7 +60,19 @@ export class Junior extends BaseEntity {
|
||||
guardian!: Guardian;
|
||||
|
||||
@OneToMany(() => Task, (task) => task.assignedTo)
|
||||
tasks?: Task[];
|
||||
tasks!: Task[];
|
||||
|
||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.junior)
|
||||
goals!: SavingGoal[];
|
||||
|
||||
@OneToMany(() => Category, (category) => category.junior)
|
||||
categories!: Category[];
|
||||
|
||||
@OneToMany(() => JuniorRegistrationToken, (token) => token.junior)
|
||||
registrationTokens!: JuniorRegistrationToken[];
|
||||
|
||||
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
|
||||
moneyRequests!: MoneyRequest[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
9
src/junior/enums/guardian-relationship.enum.ts
Normal file
9
src/junior/enums/guardian-relationship.enum.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ObjectValues } from '~/core/types';
|
||||
import { Relationship } from './relationship.enum';
|
||||
|
||||
export const GuardianRelationship = {
|
||||
[Relationship.PARENT]: 'CHILD',
|
||||
[Relationship.GUARDIAN]: 'WARD',
|
||||
} as const;
|
||||
|
||||
export type GuardianRelationship = ObjectValues<typeof GuardianRelationship>;
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './guardian-relationship.enum';
|
||||
export * from './relationship.enum';
|
||||
export * from './theme-color.enum';
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '~/auth/auth.module';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { JuniorController } from './controllers';
|
||||
import { Junior, Theme } from './entities';
|
||||
import { JuniorRepository } from './repositories';
|
||||
import { JuniorService } from './services';
|
||||
import { Junior, JuniorRegistrationToken, Theme } from './entities';
|
||||
import { JuniorRepository, JuniorTokenRepository } from './repositories';
|
||||
import { JuniorService, JuniorTokenService, QrcodeService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [JuniorController],
|
||||
providers: [JuniorService, JuniorRepository],
|
||||
imports: [TypeOrmModule.forFeature([Junior, Theme]), AuthModule, CustomerModule],
|
||||
providers: [JuniorService, JuniorRepository, JuniorTokenService, JuniorTokenRepository, QrcodeService],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Junior, Theme, JuniorRegistrationToken]),
|
||||
forwardRef(() => AuthModule),
|
||||
CustomerModule,
|
||||
],
|
||||
exports: [JuniorTokenService, JuniorService],
|
||||
})
|
||||
export class JuniorModule {}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from './junior-token.repository';
|
||||
export * from './junior.repository';
|
||||
|
||||
31
src/junior/repositories/junior-token.repository.ts
Normal file
31
src/junior/repositories/junior-token.repository.ts
Normal file
@ -0,0 +1,31 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@ -19,10 +19,14 @@ export class JuniorRepository {
|
||||
});
|
||||
}
|
||||
|
||||
findJuniorById(juniorId: string, guardianId?: string) {
|
||||
findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) {
|
||||
const relations = ['customer', 'customer.user', 'theme', 'theme.avatar'];
|
||||
if (withGuardianRelation) {
|
||||
relations.push('guardian', 'guardian.customer', 'guardian.customer.user');
|
||||
}
|
||||
return this.juniorRepository.findOne({
|
||||
where: { id: juniorId, guardianId },
|
||||
relations: ['customer', 'customer.user', 'theme'],
|
||||
relations,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from './junior-token.service';
|
||||
export * from './junior.service';
|
||||
export * from './qrcode.service';
|
||||
|
||||
33
src/junior/services/junior-token.service.ts
Normal file
33
src/junior/services/junior-token.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { JuniorTokenRepository } from '../repositories';
|
||||
import { QrcodeService } from './qrcode.service';
|
||||
|
||||
@Injectable()
|
||||
export class JuniorTokenService {
|
||||
constructor(
|
||||
private readonly juniorTokenRepository: JuniorTokenRepository,
|
||||
private readonly qrCodeService: QrcodeService,
|
||||
) {}
|
||||
async generateToken(juniorId: string): Promise<string> {
|
||||
const tokenEntity = await this.juniorTokenRepository.generateToken(juniorId);
|
||||
return this.qrCodeService.generateQrCode(tokenEntity.token);
|
||||
}
|
||||
|
||||
async validateToken(token: string) {
|
||||
const tokenEntity = await this.juniorTokenRepository.findByToken(token);
|
||||
|
||||
if (!tokenEntity) {
|
||||
throw new BadRequestException('TOKEN.INVALID');
|
||||
}
|
||||
|
||||
if (tokenEntity.expiryDate < new Date()) {
|
||||
throw new BadRequestException('TOKEN.EXPIRED');
|
||||
}
|
||||
|
||||
return tokenEntity.juniorId;
|
||||
}
|
||||
|
||||
invalidateToken(token: string) {
|
||||
return this.juniorTokenRepository.invalidateToken(token);
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,12 @@ import { Transactional } from 'typeorm-transactional';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { UserService } from '~/auth/services';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
|
||||
import { Junior } from '../entities';
|
||||
import { JuniorRepository } from '../repositories';
|
||||
import { JuniorTokenService } from './junior-token.service';
|
||||
|
||||
@Injectable()
|
||||
export class JuniorService {
|
||||
@ -14,6 +16,7 @@ export class JuniorService {
|
||||
private readonly juniorRepository: JuniorRepository,
|
||||
private readonly userService: UserService,
|
||||
private readonly customerService: CustomerService,
|
||||
private readonly juniorTokenService: JuniorTokenService,
|
||||
) {}
|
||||
|
||||
@Transactional()
|
||||
@ -43,15 +46,16 @@ export class JuniorService {
|
||||
civilIdFrontId: body.civilIdFrontId,
|
||||
civilIdBackId: body.civilIdBackId,
|
||||
}),
|
||||
notificationSettings: new CustomerNotificationSettings(),
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return this.findJuniorById(user.id, guardianId);
|
||||
return this.juniorTokenService.generateToken(user.id);
|
||||
}
|
||||
|
||||
async findJuniorById(juniorId: string, guardianId?: string) {
|
||||
const junior = await this.juniorRepository.findJuniorById(juniorId, guardianId);
|
||||
async findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) {
|
||||
const junior = await this.juniorRepository.findJuniorById(juniorId, withGuardianRelation, guardianId);
|
||||
|
||||
if (!junior) {
|
||||
throw new BadRequestException('JUNIOR.NOT_FOUND');
|
||||
@ -72,4 +76,14 @@ export class JuniorService {
|
||||
findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) {
|
||||
return this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions);
|
||||
}
|
||||
|
||||
async validateToken(token: string) {
|
||||
const juniorId = await this.juniorTokenService.validateToken(token);
|
||||
|
||||
return this.findJuniorById(juniorId, true);
|
||||
}
|
||||
|
||||
generateToken(juniorId: string) {
|
||||
return this.juniorTokenService.generateToken(juniorId);
|
||||
}
|
||||
}
|
||||
|
||||
9
src/junior/services/qrcode.service.ts
Normal file
9
src/junior/services/qrcode.service.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as qrcode from 'qrcode';
|
||||
|
||||
@Injectable()
|
||||
export class QrcodeService {
|
||||
generateQrCode(token: string): Promise<string> {
|
||||
return qrcode.toDataURL(token);
|
||||
}
|
||||
}
|
||||
1
src/money-request/controllers/index.ts
Normal file
1
src/money-request/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './money-requests.controller';
|
||||
81
src/money-request/controllers/money-requests.controller.ts
Normal file
81
src/money-request/controllers/money-requests.controller.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } 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 { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequestResponseDto } from '../dtos/response/money-request.response.dto';
|
||||
import { MoneyRequestsService } from '../services';
|
||||
|
||||
@Controller('money-requests')
|
||||
@ApiTags('Money Requests')
|
||||
@ApiBearerAuth()
|
||||
export class MoneyRequestsController {
|
||||
constructor(private readonly moneyRequestsService: MoneyRequestsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@ApiDataResponse(MoneyRequestResponseDto)
|
||||
async createMoneyRequest(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateMoneyRequestRequestDto) {
|
||||
const moneyRequest = await this.moneyRequestsService.createMoneyRequest(sub, body);
|
||||
|
||||
return ResponseFactory.data(new MoneyRequestResponseDto(moneyRequest));
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Get()
|
||||
@ApiDataPageResponse(MoneyRequestResponseDto)
|
||||
async findMoneyRequests(@AuthenticatedUser() { sub }: IJwtPayload, @Query() filters: MoneyRequestsFiltersRequestDto) {
|
||||
const [moneyRequests, itemCount] = await this.moneyRequestsService.findMoneyRequests(sub, filters);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
moneyRequests.map((moneyRequest) => new MoneyRequestResponseDto(moneyRequest)),
|
||||
{
|
||||
itemCount,
|
||||
page: filters.page,
|
||||
size: filters.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Get(':moneyRequestId')
|
||||
@ApiDataResponse(MoneyRequestResponseDto)
|
||||
async findMoneyRequestById(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('moneyRequestId', CustomParseUUIDPipe) moneyRequestId: string,
|
||||
) {
|
||||
const moneyRequest = await this.moneyRequestsService.findMoneyRequestById(moneyRequestId, sub);
|
||||
|
||||
return ResponseFactory.data(new MoneyRequestResponseDto(moneyRequest));
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Patch(':moneyRequestId/approve')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async approveMoneyRequest(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('moneyRequestId', CustomParseUUIDPipe) moneyRequestId: string,
|
||||
) {
|
||||
await this.moneyRequestsService.approveMoneyRequest(moneyRequestId, sub);
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@Patch(':moneyRequestId/reject')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async rejectMoneyRequest(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('moneyRequestId', CustomParseUUIDPipe) moneyRequestId: string,
|
||||
) {
|
||||
await this.moneyRequestsService.rejectMoneyRequest(moneyRequestId, sub);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { MoneyRequestFrequency } from '~/money-request/enums';
|
||||
const MIN_REQUESTED_AMOUNT = 0.01;
|
||||
const MAX_REQUESTED_AMOUNT = 1000000;
|
||||
export class CreateMoneyRequestRequestDto {
|
||||
@Min(MIN_REQUESTED_AMOUNT, {
|
||||
message: i18n('validation.Min', { path: 'general', property: 'moneyRequest.requestedAmount' }),
|
||||
})
|
||||
@Max(MAX_REQUESTED_AMOUNT, {
|
||||
message: i18n('validation.Max', { path: 'general', property: 'moneyRequest.requestedAmount' }),
|
||||
})
|
||||
@IsNumber(
|
||||
{ allowNaN: false },
|
||||
{ message: i18n('validation.IsNumber', { path: 'general', property: 'moneyRequest.requestedAmount' }) },
|
||||
)
|
||||
@ApiProperty()
|
||||
requestedAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'moneyRequest.message' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'moneyRequest.message' }) })
|
||||
message!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: MoneyRequestFrequency.ONE_TIME })
|
||||
@IsEnum(MoneyRequestFrequency, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'moneyRequest.frequency' }),
|
||||
})
|
||||
@IsOptional()
|
||||
frequency: MoneyRequestFrequency = MoneyRequestFrequency.ONE_TIME;
|
||||
|
||||
@ApiPropertyOptional({ example: '2021-01-01' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'moneyRequest.endDate' }) })
|
||||
@IsOptional()
|
||||
endDate?: string;
|
||||
}
|
||||
2
src/money-request/dtos/request/index.ts
Normal file
2
src/money-request/dtos/request/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './create-money-request.request.dto';
|
||||
export * from './money-requests-filters.request.dto';
|
||||
@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { MoneyRequestStatus } from '~/money-request/enums';
|
||||
export class MoneyRequestsFiltersRequestDto extends PageOptionsRequestDto {
|
||||
@ApiProperty({ example: MoneyRequestStatus.PENDING, enum: MoneyRequestStatus })
|
||||
@IsEnum(MoneyRequestStatus, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'moneyRequest.status' }),
|
||||
})
|
||||
status: MoneyRequestStatus = MoneyRequestStatus.PENDING;
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||
import { MoneyRequest } from '~/money-request/entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '~/money-request/enums';
|
||||
|
||||
export class MoneyRequestResponseDto {
|
||||
@ApiProperty({ example: 'f5c7e193-bc5e-4aa5-837b-c1edc6449880' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ type: JuniorResponseDto })
|
||||
requester!: JuniorResponseDto;
|
||||
|
||||
@ApiProperty({ example: 'f5c7e193-bc5e-4aa5-837b-c1edc6449880' })
|
||||
reviewerId!: string;
|
||||
|
||||
@ApiProperty({ example: 100.0 })
|
||||
requestedAmount!: number;
|
||||
|
||||
@ApiProperty({ example: 'Please give me money' })
|
||||
message!: string;
|
||||
|
||||
@ApiProperty({ example: MoneyRequestFrequency.ONE_TIME })
|
||||
frequency!: MoneyRequestFrequency;
|
||||
|
||||
@ApiProperty({ example: '2021-01-01' })
|
||||
endDate!: Date | null;
|
||||
|
||||
@ApiProperty({ example: MoneyRequestStatus.PENDING })
|
||||
status!: MoneyRequestStatus;
|
||||
|
||||
@ApiProperty()
|
||||
reviewedAt!: Date | null;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(moneyRequest: MoneyRequest) {
|
||||
this.id = moneyRequest.id;
|
||||
this.requester = new JuniorResponseDto(moneyRequest.requester);
|
||||
this.reviewerId = moneyRequest.reviewerId;
|
||||
this.requestedAmount = moneyRequest.requestedAmount;
|
||||
this.message = moneyRequest.message;
|
||||
this.frequency = moneyRequest.frequency;
|
||||
this.endDate = moneyRequest.endDate || null;
|
||||
this.status = moneyRequest.status;
|
||||
this.reviewedAt = moneyRequest.reviewedAt || null;
|
||||
this.createdAt = moneyRequest.createdAt;
|
||||
}
|
||||
}
|
||||
1
src/money-request/entities/index.ts
Normal file
1
src/money-request/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './money-request.entity';
|
||||
56
src/money-request/entities/money-request.entity.ts
Normal file
56
src/money-request/entities/money-request.entity.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '../enums';
|
||||
|
||||
@Entity('money_requests')
|
||||
export class MoneyRequest {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 3, name: 'requested_amount' })
|
||||
requestedAmount!: number;
|
||||
|
||||
@Column({ type: 'varchar', name: 'message' })
|
||||
message!: string;
|
||||
|
||||
@Column({ type: 'varchar', name: 'frequency', default: MoneyRequestFrequency.ONE_TIME })
|
||||
frequency!: MoneyRequestFrequency;
|
||||
|
||||
@Column({ type: 'varchar', name: 'status', default: MoneyRequestStatus.PENDING })
|
||||
status!: MoneyRequestStatus;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'reviewed_at', nullable: true })
|
||||
reviewedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'end_date', nullable: true })
|
||||
endDate!: Date | null;
|
||||
|
||||
@Column({ type: 'uuid', name: 'requester_id' })
|
||||
requesterId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'reviewer_id' })
|
||||
reviewerId!: string;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.moneyRequests)
|
||||
@JoinColumn({ name: 'requester_id' })
|
||||
requester!: Junior;
|
||||
|
||||
@ManyToOne(() => Guardian, (guardian) => guardian.moneyRequests)
|
||||
@JoinColumn({ name: 'reviewer_id' })
|
||||
reviewer!: Guardian;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp with time zone', name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp with time zone', name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
2
src/money-request/enums/index.ts
Normal file
2
src/money-request/enums/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './money-request-frequency.enum';
|
||||
export * from './money-request-status.enum';
|
||||
6
src/money-request/enums/money-request-frequency.enum.ts
Normal file
6
src/money-request/enums/money-request-frequency.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum MoneyRequestFrequency {
|
||||
ONE_TIME = 'ONE_TIME',
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
||||
5
src/money-request/enums/money-request-status.enum.ts
Normal file
5
src/money-request/enums/money-request-status.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum MoneyRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
||||
15
src/money-request/money-request.module.ts
Normal file
15
src/money-request/money-request.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JuniorModule } from '~/junior/junior.module';
|
||||
import { MoneyRequestsController } from './controllers';
|
||||
import { MoneyRequest } from './entities';
|
||||
import { MoneyRequestsRepository } from './repositories';
|
||||
import { MoneyRequestsService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [MoneyRequestsController],
|
||||
providers: [MoneyRequestsService, MoneyRequestsRepository],
|
||||
exports: [],
|
||||
imports: [TypeOrmModule.forFeature([MoneyRequest]), JuniorModule],
|
||||
})
|
||||
export class MoneyRequestModule {}
|
||||
1
src/money-request/repositories/index.ts
Normal file
1
src/money-request/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './money-requests.repository';
|
||||
49
src/money-request/repositories/money-requests.repository.ts
Normal file
49
src/money-request/repositories/money-requests.repository.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequest } from '../entities';
|
||||
import { MoneyRequestStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class MoneyRequestsRepository {
|
||||
constructor(@InjectRepository(MoneyRequest) private readonly moneyRequestRepository: Repository<MoneyRequest>) {}
|
||||
|
||||
createMoneyRequest(requesterId: string, reviewerId: string, body: CreateMoneyRequestRequestDto) {
|
||||
return this.moneyRequestRepository.save(
|
||||
this.moneyRequestRepository.create({
|
||||
requesterId,
|
||||
reviewerId,
|
||||
requestedAmount: body.requestedAmount,
|
||||
message: body.message,
|
||||
endDate: body.endDate,
|
||||
frequency: body.frequency,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findMoneyRequestById(moneyRequestId: string, reviewerId?: string) {
|
||||
return this.moneyRequestRepository.findOne({
|
||||
where: { id: moneyRequestId, reviewerId },
|
||||
relations: ['requester', 'requester.customer', 'requester.customer.profilePicture'],
|
||||
});
|
||||
}
|
||||
|
||||
findMoneyRequests(reviewerId: string, filters: MoneyRequestsFiltersRequestDto) {
|
||||
const query = this.moneyRequestRepository.createQueryBuilder('moneyRequest');
|
||||
query.leftJoinAndSelect('moneyRequest.requester', 'requester');
|
||||
query.leftJoinAndSelect('requester.customer', 'customer');
|
||||
query.leftJoinAndSelect('customer.profilePicture', 'profilePicture');
|
||||
query.orderBy('moneyRequest.createdAt', 'DESC');
|
||||
query.where('moneyRequest.reviewerId = :reviewerId', { reviewerId });
|
||||
query.andWhere('moneyRequest.status = :status', { status: filters.status });
|
||||
query.skip((filters.page - ONE) * filters.size);
|
||||
query.take(filters.size);
|
||||
|
||||
return query.getManyAndCount();
|
||||
}
|
||||
|
||||
updateMoneyRequestStatus(moneyRequestId: string, reviewerId: string, status: MoneyRequestStatus) {
|
||||
return this.moneyRequestRepository.update({ id: moneyRequestId, reviewerId }, { status, reviewedAt: new Date() });
|
||||
}
|
||||
}
|
||||
1
src/money-request/services/index.ts
Normal file
1
src/money-request/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './money-requests.service';
|
||||
99
src/money-request/services/money-requests.service.ts
Normal file
99
src/money-request/services/money-requests.service.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { OciService } from '~/document/services';
|
||||
import { JuniorService } from '~/junior/services';
|
||||
import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request';
|
||||
import { MoneyRequest } from '../entities';
|
||||
import { MoneyRequestFrequency, MoneyRequestStatus } from '../enums';
|
||||
import { MoneyRequestsRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class MoneyRequestsService {
|
||||
constructor(
|
||||
private readonly moneyRequestsRepository: MoneyRequestsRepository,
|
||||
private readonly juniorService: JuniorService,
|
||||
private readonly ociService: OciService,
|
||||
) {}
|
||||
|
||||
async createMoneyRequest(userId: string, body: CreateMoneyRequestRequestDto) {
|
||||
if (body.frequency === MoneyRequestFrequency.ONE_TIME) {
|
||||
delete body.endDate;
|
||||
}
|
||||
|
||||
if (body.endDate && new Date(body.endDate) < new Date()) {
|
||||
throw new BadRequestException('MONEY_REQUEST.END_DATE_IN_THE_PAST');
|
||||
}
|
||||
|
||||
const junior = await this.juniorService.findJuniorById(userId, true);
|
||||
|
||||
const moneyRequest = await this.moneyRequestsRepository.createMoneyRequest(junior.id, junior.guardianId, body);
|
||||
|
||||
return this.findMoneyRequestById(moneyRequest.id);
|
||||
}
|
||||
|
||||
async findMoneyRequestById(moneyRequestId: string, reviewerId?: string) {
|
||||
const moneyRequest = await this.moneyRequestsRepository.findMoneyRequestById(moneyRequestId, reviewerId);
|
||||
|
||||
if (!moneyRequest) {
|
||||
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||
}
|
||||
|
||||
await this.prepareMoneyRequestDocument([moneyRequest]);
|
||||
|
||||
return moneyRequest;
|
||||
}
|
||||
|
||||
async findMoneyRequests(
|
||||
ReviewerId: string,
|
||||
filters: MoneyRequestsFiltersRequestDto,
|
||||
): Promise<[MoneyRequest[], number]> {
|
||||
const [moneyRequests, itemCount] = await this.moneyRequestsRepository.findMoneyRequests(ReviewerId, filters);
|
||||
await this.prepareMoneyRequestDocument(moneyRequests);
|
||||
return [moneyRequests, itemCount];
|
||||
}
|
||||
|
||||
async approveMoneyRequest(moneyRequestId: string, reviewerId: string) {
|
||||
await this.validateMoneyRequestForReview(moneyRequestId, reviewerId);
|
||||
await this.moneyRequestsRepository.updateMoneyRequestStatus(
|
||||
moneyRequestId,
|
||||
reviewerId,
|
||||
MoneyRequestStatus.APPROVED,
|
||||
);
|
||||
|
||||
//@TODO send notification and update junior balance
|
||||
}
|
||||
|
||||
async rejectMoneyRequest(moneyRequestId: string, reviewerId: string) {
|
||||
await this.validateMoneyRequestForReview(moneyRequestId, reviewerId);
|
||||
|
||||
await this.moneyRequestsRepository.updateMoneyRequestStatus(
|
||||
moneyRequestId,
|
||||
reviewerId,
|
||||
MoneyRequestStatus.REJECTED,
|
||||
);
|
||||
|
||||
//@TODO send notification
|
||||
}
|
||||
|
||||
private async prepareMoneyRequestDocument(moneyRequests: MoneyRequest[]) {
|
||||
await Promise.all(
|
||||
moneyRequests.map(async (moneyRequest) => {
|
||||
const profilePicture = moneyRequest.requester.customer.profilePicture;
|
||||
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async validateMoneyRequestForReview(moneyRequestId: string, reviewerId: string) {
|
||||
const moneyRequest = await this.moneyRequestsRepository.findMoneyRequestById(moneyRequestId, reviewerId);
|
||||
|
||||
if (!moneyRequest) {
|
||||
throw new BadRequestException('MONEY_REQUEST.NOT_FOUND');
|
||||
}
|
||||
if (moneyRequest.status !== MoneyRequestStatus.PENDING) {
|
||||
throw new BadRequestException('MONEY_REQUEST.ALREADY_REVIEWED');
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/saving-goals/controllers/index.ts
Normal file
1
src/saving-goals/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './saving-goals.controller';
|
||||
95
src/saving-goals/controllers/saving-goals.controller.ts
Normal file
95
src/saving-goals/controllers/saving-goals.controller.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { createCategoryRequestDto, CreateGoalRequestDto, FundGoalRequestDto } from '../dtos/request';
|
||||
import {
|
||||
CategoriesListResponseDto,
|
||||
CategoryResponseDto,
|
||||
GoalsStatsResponseDto,
|
||||
SavingGoalDetailsResponseDto,
|
||||
} from '../dtos/response';
|
||||
import { SavingGoalsService } from '../services';
|
||||
import { CategoryService } from '../services/category.service';
|
||||
|
||||
@Controller('saving-goals')
|
||||
@ApiTags('Saving Goals')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@ApiBearerAuth()
|
||||
export class SavingGoalsController {
|
||||
constructor(
|
||||
private readonly savingGoalsService: SavingGoalsService,
|
||||
private readonly categoryService: CategoryService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiDataResponse(SavingGoalDetailsResponseDto)
|
||||
async createGoal(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateGoalRequestDto) {
|
||||
const goal = await this.savingGoalsService.createGoal(sub, body);
|
||||
|
||||
return ResponseFactory.data(new SavingGoalDetailsResponseDto(goal));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiDataPageResponse(SavingGoalDetailsResponseDto)
|
||||
async getGoals(@AuthenticatedUser() { sub }: IJwtPayload, @Query() pageOptions: PageOptionsRequestDto) {
|
||||
const [goals, itemCount] = await this.savingGoalsService.findGoals(sub, pageOptions);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
goals.map((goal) => new SavingGoalDetailsResponseDto(goal)),
|
||||
{
|
||||
page: pageOptions.page,
|
||||
size: pageOptions.size,
|
||||
itemCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiDataResponse(GoalsStatsResponseDto)
|
||||
async getStats(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
const stats = await this.savingGoalsService.getStats(sub);
|
||||
|
||||
return ResponseFactory.data(new GoalsStatsResponseDto(stats));
|
||||
}
|
||||
|
||||
@Get('categories')
|
||||
@ApiDataResponse(CategoriesListResponseDto)
|
||||
async getCategories(@AuthenticatedUser() { sub }: IJwtPayload) {
|
||||
const categories = await this.categoryService.findCategories(sub);
|
||||
|
||||
return ResponseFactory.data(new CategoriesListResponseDto(categories));
|
||||
}
|
||||
|
||||
@Get(':goalId')
|
||||
@ApiDataResponse(SavingGoalDetailsResponseDto)
|
||||
async getGoal(@AuthenticatedUser() { sub }: IJwtPayload, @Param('goalId', CustomParseUUIDPipe) goalId: string) {
|
||||
const goal = await this.savingGoalsService.findGoalById(sub, goalId);
|
||||
|
||||
return ResponseFactory.data(new SavingGoalDetailsResponseDto(goal));
|
||||
}
|
||||
|
||||
@Post('categories')
|
||||
@ApiDataResponse(CategoryResponseDto)
|
||||
async createCategory(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: createCategoryRequestDto) {
|
||||
const category = await this.categoryService.createCustomCategory(sub, body);
|
||||
return ResponseFactory.data(new CategoryResponseDto(category));
|
||||
}
|
||||
|
||||
@Post(':goalId/fund')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async fundGoal(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('goalId', CustomParseUUIDPipe) goalId: string,
|
||||
@Body() body: FundGoalRequestDto,
|
||||
) {
|
||||
await this.savingGoalsService.fundGoal(sub, goalId, body);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class createCategoryRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'category.name' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'category.name' }) })
|
||||
name!: string;
|
||||
}
|
||||
35
src/saving-goals/dtos/request/create-goal.request.dto.ts
Normal file
35
src/saving-goals/dtos/request/create-goal.request.dto.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class CreateGoalRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'goal.name' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'goal.name' }) })
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'goal.description' }) })
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '2021-12-31' })
|
||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'goal.dueDate' }) })
|
||||
dueDate!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'goal.targetAmount' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'goal.targetAmount' }) })
|
||||
targetAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsArray({ message: i18n('validation.IsArray', { path: 'general', property: 'goal.categoryIds' }) })
|
||||
@IsUUID('4', { each: true, message: i18n('validation.IsUUID', { path: 'general', property: 'goal.categoryIds' }) })
|
||||
@Transform(({ value }) => (typeof value === 'string' ? [value] : value))
|
||||
categoryIds!: string[];
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'goal.imageId' }) })
|
||||
@IsOptional()
|
||||
imageId?: string;
|
||||
}
|
||||
10
src/saving-goals/dtos/request/fund-goal.request.dto.ts
Normal file
10
src/saving-goals/dtos/request/fund-goal.request.dto.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, Min } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
const MIN_FUND = 1;
|
||||
export class FundGoalRequestDto {
|
||||
@ApiProperty({ example: '200' })
|
||||
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'goal.fundAmount' }) })
|
||||
@Min(MIN_FUND)
|
||||
fundAmount!: number;
|
||||
}
|
||||
3
src/saving-goals/dtos/request/index.ts
Normal file
3
src/saving-goals/dtos/request/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './create-category.request.dto';
|
||||
export * from './create-goal.request.dto';
|
||||
export * from './fund-goal.request.dto';
|
||||
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Category } from '~/saving-goals/entities';
|
||||
import { CategoryResponseDto } from './category-response.dto';
|
||||
|
||||
export class CategoriesListResponseDto {
|
||||
@ApiProperty({ type: CategoryResponseDto, isArray: true })
|
||||
globalCategories!: CategoryResponseDto[];
|
||||
|
||||
@ApiProperty({ type: CategoryResponseDto, isArray: true })
|
||||
customCategories!: CategoryResponseDto[];
|
||||
|
||||
constructor(data: { globalCategories: Category[]; customCategories: Category[] }) {
|
||||
this.globalCategories = data.globalCategories.map((category) => new CategoryResponseDto(category));
|
||||
this.customCategories = data.customCategories.map((category) => new CategoryResponseDto(category));
|
||||
}
|
||||
}
|
||||
19
src/saving-goals/dtos/response/category-response.dto.ts
Normal file
19
src/saving-goals/dtos/response/category-response.dto.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CategoryType } from '~/saving-goals/enums';
|
||||
|
||||
export class CategoryResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: CategoryType.CUSTOM })
|
||||
type!: CategoryType;
|
||||
|
||||
constructor(data: CategoryResponseDto) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
}
|
||||
}
|
||||
15
src/saving-goals/dtos/response/goals-stats.response.dto.ts
Normal file
15
src/saving-goals/dtos/response/goals-stats.response.dto.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IGoalStats } from '~/saving-goals/interfaces';
|
||||
const ZERO = 0;
|
||||
export class GoalsStatsResponseDto {
|
||||
@ApiProperty()
|
||||
totalTarget: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalSaved: number;
|
||||
|
||||
constructor(stats: IGoalStats) {
|
||||
this.totalTarget = stats.totalTarget || ZERO;
|
||||
this.totalSaved = stats.totalSaved || ZERO;
|
||||
}
|
||||
}
|
||||
4
src/saving-goals/dtos/response/index.ts
Normal file
4
src/saving-goals/dtos/response/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './categories-list.response.dto';
|
||||
export * from './category-response.dto';
|
||||
export * from './goals-stats.response.dto';
|
||||
export * from './saving-goal-details.response.dto';
|
||||
@ -0,0 +1,49 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { SavingGoal } from '~/saving-goals/entities';
|
||||
import { CategoryResponseDto } from './category-response.dto';
|
||||
|
||||
export class SavingGoalDetailsResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
dueDate!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
targetAmount!: number;
|
||||
|
||||
@ApiProperty()
|
||||
currentAmount!: number;
|
||||
|
||||
@ApiPropertyOptional({ type: CategoryResponseDto, isArray: true })
|
||||
categories?: CategoryResponseDto[];
|
||||
|
||||
@ApiPropertyOptional({ type: DocumentMetaResponseDto, isArray: true })
|
||||
image: DocumentMetaResponseDto | null;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(data: SavingGoal) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.dueDate = data.dueDate;
|
||||
this.targetAmount = data.targetAmount;
|
||||
this.currentAmount = data.currentAmount;
|
||||
this.categories = data.categories ? data.categories.map((category) => new CategoryResponseDto(category)) : [];
|
||||
this.image = data.image ? new DocumentMetaResponseDto(data.image) : null;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
}
|
||||
}
|
||||
41
src/saving-goals/entities/category.entity.ts
Normal file
41
src/saving-goals/entities/category.entity.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { CategoryType } from '../enums';
|
||||
import { SavingGoal } from './saving-goal.entity';
|
||||
|
||||
@Entity('categories')
|
||||
export class Category {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'type' })
|
||||
type!: CategoryType;
|
||||
|
||||
@Column({ type: 'uuid', name: 'junior_id', nullable: true })
|
||||
juniorId!: string;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.categories, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
|
||||
@ManyToMany(() => SavingGoal, (savingGoal) => savingGoal.categories)
|
||||
goals!: SavingGoal[];
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
2
src/saving-goals/entities/index.ts
Normal file
2
src/saving-goals/entities/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './category.entity';
|
||||
export * from './saving-goal.entity';
|
||||
76
src/saving-goals/entities/saving-goal.entity.ts
Normal file
76
src/saving-goals/entities/saving-goal.entity.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Document } from '~/document/entities';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { Category } from './category.entity';
|
||||
const ZERO = 0;
|
||||
@Entity('saving_goals')
|
||||
export class SavingGoal extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'description', nullable: true })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'date', name: 'due_date' })
|
||||
dueDate!: Date;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
name: 'target_amount',
|
||||
precision: 12,
|
||||
scale: 3,
|
||||
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||
})
|
||||
targetAmount!: number;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
name: 'current_amount',
|
||||
precision: 12,
|
||||
scale: 3,
|
||||
default: ZERO,
|
||||
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||
})
|
||||
currentAmount!: number;
|
||||
|
||||
@Column({ type: 'uuid', name: 'image_id', nullable: true })
|
||||
imageId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'junior_id' })
|
||||
juniorId!: string;
|
||||
|
||||
@ManyToOne(() => Document, (document) => document.goals, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'image_id' })
|
||||
image!: Document;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.goals, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
|
||||
@ManyToMany(() => Category, (category) => category.goals)
|
||||
@JoinTable({
|
||||
name: 'saving_goals_categories',
|
||||
joinColumn: { name: 'saving_goal_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'category_id', referencedColumnName: 'id' },
|
||||
})
|
||||
categories!: Category[];
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'created_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp', name: 'updated_at', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
4
src/saving-goals/enums/category-type.enum.ts
Normal file
4
src/saving-goals/enums/category-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class CategoryType {
|
||||
static readonly GLOBAL = 'GLOBAL';
|
||||
static readonly CUSTOM = 'CUSTOM';
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user