mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 21:59:40 +00:00
feat: working on saving goals jounrey for juniors
This commit is contained in:
@ -20,6 +20,7 @@ import { DocumentModule } from './document/document.module';
|
|||||||
import { GuardianModule } from './guardian/guardian.module';
|
import { GuardianModule } from './guardian/guardian.module';
|
||||||
import { HealthModule } from './health/health.module';
|
import { HealthModule } from './health/health.module';
|
||||||
import { JuniorModule } from './junior/junior.module';
|
import { JuniorModule } from './junior/junior.module';
|
||||||
|
import { SavingGoalsModule } from './saving-goals/saving-goals.module';
|
||||||
import { TaskModule } from './task/task.module';
|
import { TaskModule } from './task/task.module';
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [],
|
controllers: [],
|
||||||
@ -54,6 +55,8 @@ import { TaskModule } from './task/task.module';
|
|||||||
JuniorModule,
|
JuniorModule,
|
||||||
TaskModule,
|
TaskModule,
|
||||||
GuardianModule,
|
GuardianModule,
|
||||||
|
SavingGoalsModule,
|
||||||
|
|
||||||
OtpModule,
|
OtpModule,
|
||||||
DocumentModule,
|
DocumentModule,
|
||||||
LookupModule,
|
LookupModule,
|
||||||
|
@ -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}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,3 +10,5 @@ export * from './1733750228289-seed-default-avatar';
|
|||||||
export * from './1733904556416-create-task-entities';
|
export * from './1733904556416-create-task-entities';
|
||||||
export * from './1733990253208-seeds-default-tasks-logo';
|
export * from './1733990253208-seeds-default-tasks-logo';
|
||||||
export * from './1733993920226-create-customer-notifications-settings-table';
|
export * from './1733993920226-create-customer-notifications-settings-table';
|
||||||
|
export * from './1734246386471-create-saving-goals-entities';
|
||||||
|
export * from './1734247702310-seeds-goals-categories';
|
||||||
|
@ -7,4 +7,5 @@ export const BUCKETS: Record<DocumentType, string> = {
|
|||||||
[DocumentType.DEFAULT_TASKS_LOGO]: 'tasks-logo',
|
[DocumentType.DEFAULT_TASKS_LOGO]: 'tasks-logo',
|
||||||
[DocumentType.CUSTOM_AVATAR]: 'avatars',
|
[DocumentType.CUSTOM_AVATAR]: 'avatars',
|
||||||
[DocumentType.CUSTOM_TASKS_LOGO]: 'tasks-logo',
|
[DocumentType.CUSTOM_TASKS_LOGO]: 'tasks-logo',
|
||||||
|
[DocumentType.GOALS]: 'goals',
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDate
|
|||||||
import { User } from '~/auth/entities';
|
import { User } from '~/auth/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Junior, Theme } from '~/junior/entities';
|
import { Junior, Theme } from '~/junior/entities';
|
||||||
|
import { SavingGoal } from '~/saving-goals/entities';
|
||||||
import { Task } from '~/task/entities';
|
import { Task } from '~/task/entities';
|
||||||
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
|
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
|
||||||
import { DocumentType } from '../enums';
|
import { DocumentType } from '../enums';
|
||||||
@ -38,6 +39,9 @@ export class Document {
|
|||||||
@OneToMany(() => TaskSubmission, (taskSubmission) => taskSubmission.proofOfCompletion)
|
@OneToMany(() => TaskSubmission, (taskSubmission) => taskSubmission.proofOfCompletion)
|
||||||
submissions?: TaskSubmission[];
|
submissions?: TaskSubmission[];
|
||||||
|
|
||||||
|
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
|
||||||
|
goals?: SavingGoal[];
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@ -5,4 +5,5 @@ export enum DocumentType {
|
|||||||
DEFAULT_TASKS_LOGO = 'DEFAULT_TASKS_LOGO',
|
DEFAULT_TASKS_LOGO = 'DEFAULT_TASKS_LOGO',
|
||||||
CUSTOM_AVATAR = 'CUSTOM_AVATAR',
|
CUSTOM_AVATAR = 'CUSTOM_AVATAR',
|
||||||
CUSTOM_TASKS_LOGO = 'CUSTOM_TASKS_LOGO',
|
CUSTOM_TASKS_LOGO = 'CUSTOM_TASKS_LOGO',
|
||||||
|
GOALS = 'GOALS',
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Document } from '~/document/entities';
|
import { Document } from '~/document/entities';
|
||||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { Category, SavingGoal } from '~/saving-goals/entities';
|
||||||
import { Task } from '~/task/entities';
|
import { Task } from '~/task/entities';
|
||||||
import { Relationship } from '../enums';
|
import { Relationship } from '../enums';
|
||||||
import { Theme } from './theme.entity';
|
import { Theme } from './theme.entity';
|
||||||
@ -59,6 +60,12 @@ export class Junior extends BaseEntity {
|
|||||||
@OneToMany(() => Task, (task) => task.assignedTo)
|
@OneToMany(() => Task, (task) => task.assignedTo)
|
||||||
tasks?: Task[];
|
tasks?: Task[];
|
||||||
|
|
||||||
|
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.junior)
|
||||||
|
goals?: SavingGoal[];
|
||||||
|
|
||||||
|
@OneToMany(() => Category, (category) => category.junior)
|
||||||
|
categories?: Category[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
1
src/saving-goals/enums/index.ts
Normal file
1
src/saving-goals/enums/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './category-type.enum';
|
4
src/saving-goals/interfaces/goal-stats.interface.ts
Normal file
4
src/saving-goals/interfaces/goal-stats.interface.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface IGoalStats {
|
||||||
|
totalTarget: number;
|
||||||
|
totalSaved: number;
|
||||||
|
}
|
1
src/saving-goals/interfaces/index.ts
Normal file
1
src/saving-goals/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './goal-stats.interface';
|
36
src/saving-goals/repositories/category.repository.ts
Normal file
36
src/saving-goals/repositories/category.repository.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { In, Repository } from 'typeorm';
|
||||||
|
import { createCategoryRequestDto } from '../dtos/request';
|
||||||
|
import { Category } from '../entities';
|
||||||
|
import { CategoryType } from '../enums';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CategoryRepository {
|
||||||
|
constructor(@InjectRepository(Category) private categoryRepository: Repository<Category>) {}
|
||||||
|
|
||||||
|
createCustomCategory(juniorId: string, body: createCategoryRequestDto): Promise<Category> {
|
||||||
|
return this.categoryRepository.save(
|
||||||
|
this.categoryRepository.create({
|
||||||
|
juniorId,
|
||||||
|
name: body.name,
|
||||||
|
type: CategoryType.CUSTOM,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findCategoryById(ids: string[], juniorId: string): Promise<Category[]> {
|
||||||
|
return this.categoryRepository.find({
|
||||||
|
where: [
|
||||||
|
{ id: In(ids), type: CategoryType.GLOBAL },
|
||||||
|
{ id: In(ids), juniorId, type: CategoryType.CUSTOM },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findCategories(juniorId: string): Promise<Category[]> {
|
||||||
|
return this.categoryRepository.find({
|
||||||
|
where: [{ type: 'GLOBAL' }, { juniorId, type: CategoryType.CUSTOM }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
2
src/saving-goals/repositories/index.ts
Normal file
2
src/saving-goals/repositories/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './category.repository';
|
||||||
|
export * from './saving-goals.repository';
|
55
src/saving-goals/repositories/saving-goals.repository.ts
Normal file
55
src/saving-goals/repositories/saving-goals.repository.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { CreateGoalRequestDto } from '../dtos/request';
|
||||||
|
import { Category, SavingGoal } from '../entities';
|
||||||
|
const ZERO = 0;
|
||||||
|
const ONE = 1;
|
||||||
|
@Injectable()
|
||||||
|
export class SavingGoalsRepository {
|
||||||
|
constructor(@InjectRepository(SavingGoal) private readonly savingGoalRepository: Repository<SavingGoal>) {}
|
||||||
|
|
||||||
|
createGoal(juniorId: string, body: CreateGoalRequestDto, categories: Category[]) {
|
||||||
|
return this.savingGoalRepository.save(
|
||||||
|
this.savingGoalRepository.create({
|
||||||
|
name: body.name,
|
||||||
|
description: body.description,
|
||||||
|
dueDate: body.dueDate,
|
||||||
|
targetAmount: body.targetAmount,
|
||||||
|
juniorId,
|
||||||
|
imageId: body.imageId,
|
||||||
|
categories,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findGoals(juniorId: string, pageOptions: PageOptionsRequestDto) {
|
||||||
|
return this.savingGoalRepository.findAndCount({
|
||||||
|
where: { juniorId },
|
||||||
|
take: pageOptions.size,
|
||||||
|
skip: pageOptions.size * (pageOptions.page - ONE),
|
||||||
|
relations: ['categories', 'image'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findGoalById(juniorId: string, goalId: string) {
|
||||||
|
return this.savingGoalRepository.findOneOrFail({
|
||||||
|
where: { juniorId, id: goalId },
|
||||||
|
relations: ['categories', 'image'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fundGoal(juniorId: string, goalId: string, amount: number) {
|
||||||
|
return this.savingGoalRepository.increment({ juniorId, id: goalId }, 'currentAmount', amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats(juniorId: string) {
|
||||||
|
return this.savingGoalRepository
|
||||||
|
.createQueryBuilder('savingGoal')
|
||||||
|
.select('SUM(savingGoal.currentAmount)', 'totalSaved')
|
||||||
|
.addSelect('SUM(savingGoal.targetAmount)', 'totalTarget')
|
||||||
|
.where('savingGoal.juniorId = :juniorId', { juniorId })
|
||||||
|
.getRawOne();
|
||||||
|
}
|
||||||
|
}
|
14
src/saving-goals/saving-goals.module.ts
Normal file
14
src/saving-goals/saving-goals.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SavingGoalsController } from './controllers';
|
||||||
|
import { Category, SavingGoal } from './entities';
|
||||||
|
import { CategoryRepository, SavingGoalsRepository } from './repositories';
|
||||||
|
import { SavingGoalsService } from './services';
|
||||||
|
import { CategoryService } from './services/category.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([SavingGoal, Category])],
|
||||||
|
providers: [SavingGoalsService, SavingGoalsRepository, CategoryService, CategoryRepository],
|
||||||
|
controllers: [SavingGoalsController],
|
||||||
|
})
|
||||||
|
export class SavingGoalsModule {}
|
35
src/saving-goals/services/category.service.ts
Normal file
35
src/saving-goals/services/category.service.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { createCategoryRequestDto } from '../dtos/request';
|
||||||
|
import { Category } from '../entities';
|
||||||
|
import { CategoryType } from '../enums';
|
||||||
|
import { CategoryRepository } from '../repositories';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CategoryService {
|
||||||
|
constructor(private readonly categoryRepository: CategoryRepository) {}
|
||||||
|
|
||||||
|
createCustomCategory(juniorId: string, body: createCategoryRequestDto) {
|
||||||
|
return this.categoryRepository.createCustomCategory(juniorId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCategoryByIds(categoryIds: string[], juniorId: string) {
|
||||||
|
const categories = await this.categoryRepository.findCategoryById(categoryIds, juniorId);
|
||||||
|
|
||||||
|
if (categories.length !== categoryIds.length) {
|
||||||
|
throw new BadRequestException('CATEGORY.NOT_FOUND');
|
||||||
|
}
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCategories(juniorId: string): Promise<{ globalCategories: Category[]; customCategories: Category[] }> {
|
||||||
|
const categories = await this.categoryRepository.findCategories(juniorId);
|
||||||
|
|
||||||
|
const globalCategories = categories.filter((category) => category.type === CategoryType.GLOBAL);
|
||||||
|
const customCategories = categories.filter((category) => category.type === CategoryType.CUSTOM);
|
||||||
|
|
||||||
|
return {
|
||||||
|
globalCategories,
|
||||||
|
customCategories,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
1
src/saving-goals/services/index.ts
Normal file
1
src/saving-goals/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './saving-goals.service';
|
81
src/saving-goals/services/saving-goals.service.ts
Normal file
81
src/saving-goals/services/saving-goals.service.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { CreateGoalRequestDto, FundGoalRequestDto } from '../dtos/request';
|
||||||
|
import { SavingGoal } from '../entities';
|
||||||
|
import { IGoalStats } from '../interfaces';
|
||||||
|
import { SavingGoalsRepository } from '../repositories';
|
||||||
|
import { CategoryService } from './category.service';
|
||||||
|
const ZERO = 0;
|
||||||
|
@Injectable()
|
||||||
|
export class SavingGoalsService {
|
||||||
|
constructor(
|
||||||
|
private readonly savingGoalsRepository: SavingGoalsRepository,
|
||||||
|
private readonly categoryService: CategoryService,
|
||||||
|
private readonly ociService: OciService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createGoal(juniorId: string, body: CreateGoalRequestDto) {
|
||||||
|
if (moment(body.dueDate).isBefore(moment())) {
|
||||||
|
throw new BadRequestException('GOAL.DUE_DATE_MUST_BE_IN_THE_FUTURE');
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = await this.categoryService.findCategoryByIds(body.categoryIds, juniorId);
|
||||||
|
|
||||||
|
const createdGoal = await this.savingGoalsRepository.createGoal(juniorId, body, categories);
|
||||||
|
|
||||||
|
return this.findGoalById(juniorId, createdGoal.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findGoals(juniorId: string, pageOptions: PageOptionsRequestDto): Promise<[SavingGoal[], number]> {
|
||||||
|
const [goals, itemCount] = await this.savingGoalsRepository.findGoals(juniorId, pageOptions);
|
||||||
|
await this.prepareGoalsImages(goals);
|
||||||
|
|
||||||
|
return [goals, itemCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findGoalById(juniorId: string, goalId: string) {
|
||||||
|
const goal = await this.savingGoalsRepository.findGoalById(juniorId, goalId);
|
||||||
|
|
||||||
|
if (!goal) {
|
||||||
|
throw new BadRequestException('GOAL.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prepareGoalsImages([goal]);
|
||||||
|
|
||||||
|
return goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fundGoal(juniorId: string, goalId: string, body: FundGoalRequestDto) {
|
||||||
|
const goal = await this.savingGoalsRepository.findGoalById(juniorId, goalId);
|
||||||
|
|
||||||
|
if (!goal) {
|
||||||
|
throw new BadRequestException('GOAL.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goal.currentAmount + body.fundAmount > goal.targetAmount) {
|
||||||
|
throw new BadRequestException('GOAL.FUND_EXCEEDS_TOTAL_AMOUNT');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.savingGoalsRepository.fundGoal(juniorId, goalId, body.fundAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStats(juniorId: string): Promise<IGoalStats> {
|
||||||
|
const result = await this.savingGoalsRepository.getStats(juniorId);
|
||||||
|
return {
|
||||||
|
totalTarget: result?.totalTarget,
|
||||||
|
totalSaved: result?.totalSaved,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareGoalsImages(goals: SavingGoal[]) {
|
||||||
|
await Promise.all(
|
||||||
|
goals.map(async (goal) => {
|
||||||
|
if (goal.imageId) {
|
||||||
|
goal.image.url = await this.ociService.generatePreSignedUrl(goal.image);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,6 @@ export class TaskController {
|
|||||||
@AllowedRoles(Roles.GUARDIAN)
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
async createTask(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateTaskRequestDto) {
|
async createTask(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateTaskRequestDto) {
|
||||||
const task = await this.taskService.createTask(sub, body);
|
const task = await this.taskService.createTask(sub, body);
|
||||||
console.log(task.dueDate);
|
|
||||||
return ResponseFactory.data(new TaskResponseDto(task));
|
return ResponseFactory.data(new TaskResponseDto(task));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ export class TaskService {
|
|||||||
|
|
||||||
async findTasks(user: IJwtPayload, query: TasksFilterOptions): Promise<[Task[], number]> {
|
async findTasks(user: IJwtPayload, query: TasksFilterOptions): Promise<[Task[], number]> {
|
||||||
const [tasks, count] = await this.taskRepository.findTasks(user, query);
|
const [tasks, count] = await this.taskRepository.findTasks(user, query);
|
||||||
console.log(tasks);
|
|
||||||
await this.prepareTasksPictures(tasks);
|
await this.prepareTasksPictures(tasks);
|
||||||
|
|
||||||
return [tasks, count];
|
return [tasks, count];
|
||||||
|
Reference in New Issue
Block a user