mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 21:59:40 +00:00
feat: gift journeys
This commit is contained in:
@ -18,6 +18,7 @@ import { buildValidationPipe } from './core/pipes';
|
|||||||
import { CustomerModule } from './customer/customer.module';
|
import { CustomerModule } from './customer/customer.module';
|
||||||
import { migrations } from './db';
|
import { migrations } from './db';
|
||||||
import { DocumentModule } from './document/document.module';
|
import { DocumentModule } from './document/document.module';
|
||||||
|
import { GiftModule } from './gift/gift.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';
|
||||||
@ -61,8 +62,8 @@ import { TaskModule } from './task/task.module';
|
|||||||
GuardianModule,
|
GuardianModule,
|
||||||
SavingGoalsModule,
|
SavingGoalsModule,
|
||||||
AllowanceModule,
|
AllowanceModule,
|
||||||
|
|
||||||
MoneyRequestModule,
|
MoneyRequestModule,
|
||||||
|
GiftModule,
|
||||||
|
|
||||||
OtpModule,
|
OtpModule,
|
||||||
DocumentModule,
|
DocumentModule,
|
||||||
|
66
src/db/migrations/1734861516657-create-gift-entities.ts
Normal file
66
src/db/migrations/1734861516657-create-gift-entities.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateGiftEntities1734861516657 implements MigrationInterface {
|
||||||
|
name = 'CreateGiftEntities1734861516657';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "gift_replies"
|
||||||
|
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"status" character varying NOT NULL DEFAULT 'PENDING',
|
||||||
|
"color" character varying NOT NULL,
|
||||||
|
"gift_id" uuid NOT NULL,
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT "REL_8292da97f1ceb9a806b8bc812f" UNIQUE ("gift_id"),
|
||||||
|
CONSTRAINT "PK_ec6567bb5ab318bb292fa6599a2" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "gift"
|
||||||
|
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"name" character varying(255) NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"color" character varying NOT NULL,
|
||||||
|
"amount" numeric(10,3) NOT NULL,
|
||||||
|
"status" character varying NOT NULL DEFAULT 'AVAILABLE',
|
||||||
|
"redeemed_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
"giver_id" uuid NOT NULL, "image_id" uuid NOT NULL,
|
||||||
|
"recipient_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_f91217caddc01a085837ebe0606" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "gift_redemptions"
|
||||||
|
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"gift_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_6ad7ac76169c3a224ce4a3afff4" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "gift_replies" ADD CONSTRAINT "FK_8292da97f1ceb9a806b8bc812f2" FOREIGN KEY ("gift_id") REFERENCES "gift"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "gift" ADD CONSTRAINT "FK_0d317b68508819308455db9b9be" FOREIGN KEY ("giver_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "gift" ADD CONSTRAINT "FK_4a46b5734fb573dc956904c18d0" FOREIGN KEY ("recipient_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "gift" ADD CONSTRAINT "FK_83bb54c127d0e6ee487b90e2996" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "gift_redemptions" ADD CONSTRAINT "FK_243c4349f0c45ce5385ac316aaa" FOREIGN KEY ("gift_id") REFERENCES "gift"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "gift_redemptions" DROP CONSTRAINT "FK_243c4349f0c45ce5385ac316aaa"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "gift" DROP CONSTRAINT "FK_83bb54c127d0e6ee487b90e2996"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "gift" DROP CONSTRAINT "FK_4a46b5734fb573dc956904c18d0"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "gift" DROP CONSTRAINT "FK_0d317b68508819308455db9b9be"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "gift_replies" DROP CONSTRAINT "FK_8292da97f1ceb9a806b8bc812f2"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "gift_redemptions"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "gift"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "gift_replies"`);
|
||||||
|
}
|
||||||
|
}
|
@ -15,3 +15,4 @@ export * from './1734247702310-seeds-goals-categories';
|
|||||||
export * from './1734262619426-create-junior-registration-token-table';
|
export * from './1734262619426-create-junior-registration-token-table';
|
||||||
export * from './1734503895302-create-money-request-entity';
|
export * from './1734503895302-create-money-request-entity';
|
||||||
export * from './1734601976591-create-allowance-entities';
|
export * from './1734601976591-create-allowance-entities';
|
||||||
|
export * from './1734861516657-create-gift-entities';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
import { User } from '~/auth/entities';
|
import { User } from '~/auth/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
|
import { Gift } from '~/gift/entities';
|
||||||
import { Junior, Theme } from '~/junior/entities';
|
import { Junior, Theme } from '~/junior/entities';
|
||||||
import { SavingGoal } from '~/saving-goals/entities';
|
import { SavingGoal } from '~/saving-goals/entities';
|
||||||
import { Task } from '~/task/entities';
|
import { Task } from '~/task/entities';
|
||||||
@ -42,6 +43,9 @@ export class Document {
|
|||||||
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
|
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
|
||||||
goals?: SavingGoal[];
|
goals?: SavingGoal[];
|
||||||
|
|
||||||
|
@OneToMany(() => Gift, (gift) => gift.image)
|
||||||
|
gifts?: Gift[];
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
81
src/gift/controllers/gifts.controller.ts
Normal file
81
src/gift/controllers/gifts.controller.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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 { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||||
|
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||||
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { CreateGiftRequestDto, GiftFiltersRequestDto, GiftReplyRequestDto } from '../dtos/request';
|
||||||
|
import { GiftDetailsResponseDto, GiftListResponseDto } from '../dtos/response';
|
||||||
|
import { GiftsService } from '../services';
|
||||||
|
|
||||||
|
@Controller('gift')
|
||||||
|
@ApiTags('Gifts')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class GiftsController {
|
||||||
|
constructor(private readonly giftsService: GiftsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.GUARDIAN)
|
||||||
|
@ApiDataResponse(GiftDetailsResponseDto)
|
||||||
|
async createGift(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateGiftRequestDto) {
|
||||||
|
const gift = await this.giftsService.createGift(sub, body);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new GiftDetailsResponseDto(gift));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiDataPageResponse(GiftListResponseDto)
|
||||||
|
async findGifts(@AuthenticatedUser() user: IJwtPayload, @Query() filters: GiftFiltersRequestDto) {
|
||||||
|
const [gifts, itemCount] = await this.giftsService.findGifts(user, filters);
|
||||||
|
|
||||||
|
return ResponseFactory.dataPage(
|
||||||
|
gifts.map((gift) => new GiftListResponseDto(gift)),
|
||||||
|
{
|
||||||
|
size: filters.size,
|
||||||
|
page: filters.page,
|
||||||
|
itemCount,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':giftId')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@ApiDataResponse(GiftDetailsResponseDto)
|
||||||
|
async findGiftById(@AuthenticatedUser() user: IJwtPayload, @Param('giftId') giftId: string) {
|
||||||
|
const gift = await this.giftsService.findUserGiftById(user, giftId);
|
||||||
|
|
||||||
|
return ResponseFactory.data(new GiftDetailsResponseDto(gift));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':giftId/redeem')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
redeemGift(@AuthenticatedUser() { sub }: IJwtPayload, @Param('giftId') giftId: string) {
|
||||||
|
return this.giftsService.redeemGift(sub, giftId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':giftId/undo-redeem')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
UndoGiftRedemption(@AuthenticatedUser() { sub }: IJwtPayload, @Param('giftId') giftId: string) {
|
||||||
|
return this.giftsService.UndoGiftRedemption(sub, giftId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':giftId/reply')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@AllowedRoles(Roles.JUNIOR)
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
replyToGift(
|
||||||
|
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||||
|
@Param('giftId') giftId: string,
|
||||||
|
@Body() body: GiftReplyRequestDto,
|
||||||
|
) {
|
||||||
|
return this.giftsService.replyToGift(sub, giftId, body);
|
||||||
|
}
|
||||||
|
}
|
1
src/gift/controllers/index.ts
Normal file
1
src/gift/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './gifts.controller';
|
32
src/gift/dtos/request/create-gift.request.dto.ts
Normal file
32
src/gift/dtos/request/create-gift.request.dto.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { GiftColor } from '~/gift/enums';
|
||||||
|
export class CreateGiftRequestDto {
|
||||||
|
@ApiProperty({ example: 'Happy Birthday' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'gift.name' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'gift.name' }) })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Description of the gift' })
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'gift.description' }) })
|
||||||
|
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'gift.description' }) })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: GiftColor.VIOLET })
|
||||||
|
@IsEnum(GiftColor, { message: i18n('validation.IsEnum', { path: 'general', property: 'gift.color' }) })
|
||||||
|
color!: GiftColor;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 100 })
|
||||||
|
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'gift.amount' }) })
|
||||||
|
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'gift.amount' }) })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'gift.imageId' }) })
|
||||||
|
imageId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||||
|
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'gift.recipientId' }) })
|
||||||
|
recipientId!: string;
|
||||||
|
}
|
10
src/gift/dtos/request/gift-filters.request.dto.ts
Normal file
10
src/gift/dtos/request/gift-filters.request.dto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||||
|
import { GiftStatus } from '~/gift/enums';
|
||||||
|
export class GiftFiltersRequestDto extends PageOptionsRequestDto {
|
||||||
|
@ApiProperty({ enum: GiftStatus })
|
||||||
|
@IsEnum(GiftStatus, { message: i18n('validation.IsEnum', { path: 'general', property: 'gift.status' }) })
|
||||||
|
status!: GiftStatus;
|
||||||
|
}
|
9
src/gift/dtos/request/gift-reply.request.dto.ts
Normal file
9
src/gift/dtos/request/gift-reply.request.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum } from 'class-validator';
|
||||||
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
|
import { GiftColor } from '~/gift/enums';
|
||||||
|
export class GiftReplyRequestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsEnum(GiftColor, { message: i18n('validation.IsEnum', { path: 'general', property: 'gift.color' }) })
|
||||||
|
color!: GiftColor;
|
||||||
|
}
|
3
src/gift/dtos/request/index.ts
Normal file
3
src/gift/dtos/request/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './create-gift.request.dto';
|
||||||
|
export * from './gift-filters.request.dto';
|
||||||
|
export * from './gift-reply.request.dto';
|
54
src/gift/dtos/response/gift-details.response.dto.ts
Normal file
54
src/gift/dtos/response/gift-details.response.dto.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||||
|
import { Gift } from '~/gift/entities';
|
||||||
|
import { GiftColor, GiftStatus } from '~/gift/enums';
|
||||||
|
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||||
|
|
||||||
|
export class GiftDetailsResponseDto {
|
||||||
|
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Happy Birthday' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Description of the gift' })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '200' })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: GiftStatus.AVAILABLE })
|
||||||
|
status!: GiftStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ example: GiftColor.GREEN })
|
||||||
|
color!: GiftColor;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JuniorResponseDto })
|
||||||
|
recipient!: JuniorResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Ahmad Khalid' })
|
||||||
|
giverName!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: DocumentMetaResponseDto })
|
||||||
|
image!: DocumentMetaResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2022-01-01T00:00:00.000Z' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2022-01-01T00:00:00.000Z' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
constructor(gift: Gift) {
|
||||||
|
this.id = gift.id;
|
||||||
|
this.name = gift.name;
|
||||||
|
this.description = gift.description;
|
||||||
|
this.amount = gift.amount;
|
||||||
|
this.status = gift.status;
|
||||||
|
this.color = gift.color;
|
||||||
|
this.giverName = gift.giver.customer.firstName + ' ' + gift.giver.customer.lastName;
|
||||||
|
this.recipient = new JuniorResponseDto(gift.recipient);
|
||||||
|
this.image = new DocumentMetaResponseDto(gift.image);
|
||||||
|
this.createdAt = gift.createdAt;
|
||||||
|
this.updatedAt = gift.updatedAt;
|
||||||
|
}
|
||||||
|
}
|
32
src/gift/dtos/response/gift-list.response.dto.ts
Normal file
32
src/gift/dtos/response/gift-list.response.dto.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Gift } from '~/gift/entities';
|
||||||
|
import { GiftColor, GiftStatus } from '~/gift/enums';
|
||||||
|
|
||||||
|
export class GiftListResponseDto {
|
||||||
|
@ApiProperty({ example: 'f7b9b1b0-0b3b-4b3b-8b3b-0b3b4b3b8b3b' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Happy Birthday' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '242' })
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: GiftStatus.AVAILABLE })
|
||||||
|
status!: GiftStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ example: GiftColor.GREEN })
|
||||||
|
color!: GiftColor;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2022-01-01T00:00:00.000Z' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
constructor(gift: Gift) {
|
||||||
|
this.id = gift.id;
|
||||||
|
this.name = gift.name;
|
||||||
|
this.amount = gift.amount;
|
||||||
|
this.status = gift.status;
|
||||||
|
this.color = gift.color;
|
||||||
|
this.createdAt = gift.createdAt;
|
||||||
|
}
|
||||||
|
}
|
2
src/gift/dtos/response/index.ts
Normal file
2
src/gift/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './gift-details.response.dto';
|
||||||
|
export * from './gift-list.response.dto';
|
30
src/gift/entities/gift-redemption.entity.ts
Normal file
30
src/gift/entities/gift-redemption.entity.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Gift } from './gift.entity';
|
||||||
|
|
||||||
|
@Entity('gift_redemptions')
|
||||||
|
export class GiftRedemption extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'gift_id' })
|
||||||
|
giftId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Gift, (gift) => gift.redemptions, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'gift_id' })
|
||||||
|
gift!: Gift;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
25
src/gift/entities/gift-reply.entity.ts
Normal file
25
src/gift/entities/gift-reply.entity.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { GiftColor, GiftReplyStatus } from '../enums';
|
||||||
|
import { Gift } from './gift.entity';
|
||||||
|
|
||||||
|
@Entity('gift_replies')
|
||||||
|
export class GiftReply extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'status', default: GiftReplyStatus.PENDING })
|
||||||
|
status!: GiftReplyStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'color' })
|
||||||
|
color!: GiftColor;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'gift_id' })
|
||||||
|
giftId!: string;
|
||||||
|
|
||||||
|
@OneToOne(() => Gift, (gift) => gift.reply, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'gift_id' })
|
||||||
|
gift!: Gift;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
}
|
83
src/gift/entities/gift.entity.ts
Normal file
83
src/gift/entities/gift.entity.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
OneToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Document } from '~/document/entities';
|
||||||
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
|
import { Junior } from '~/junior/entities';
|
||||||
|
import { GiftColor, GiftStatus } from '../enums';
|
||||||
|
import { GiftRedemption } from './gift-redemption.entity';
|
||||||
|
import { GiftReply } from './gift-reply.entity';
|
||||||
|
|
||||||
|
@Entity('gift')
|
||||||
|
export class Gift {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', name: 'description' })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'color' })
|
||||||
|
color!: GiftColor;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
name: 'amount',
|
||||||
|
precision: 10,
|
||||||
|
scale: 3,
|
||||||
|
transformer: {
|
||||||
|
to: (value: number) => value,
|
||||||
|
from: (value: string) => parseFloat(value),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'status', default: GiftStatus.AVAILABLE })
|
||||||
|
status!: GiftStatus;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', name: 'redeemed_at', nullable: true })
|
||||||
|
redeemedAt!: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'giver_id' })
|
||||||
|
giverId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'image_id' })
|
||||||
|
imageId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'recipient_id' })
|
||||||
|
recipientId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Guardian, (guardian) => guardian.gifts)
|
||||||
|
@JoinColumn({ name: 'giver_id' })
|
||||||
|
giver!: Guardian;
|
||||||
|
|
||||||
|
@ManyToOne(() => Junior, (junior) => junior.gifts)
|
||||||
|
@JoinColumn({ name: 'recipient_id' })
|
||||||
|
recipient!: Junior;
|
||||||
|
|
||||||
|
@ManyToOne(() => Document, (document) => document.gifts)
|
||||||
|
@JoinColumn({ name: 'image_id' })
|
||||||
|
image!: Document;
|
||||||
|
|
||||||
|
@OneToMany(() => GiftRedemption, (giftRedemption) => giftRedemption.gift, { cascade: true })
|
||||||
|
redemptions!: GiftRedemption[];
|
||||||
|
|
||||||
|
@OneToOne(() => GiftReply, (giftReply) => giftReply.gift, { cascade: true })
|
||||||
|
reply?: GiftReply;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
3
src/gift/entities/index.ts
Normal file
3
src/gift/entities/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './gift-redemption.entity';
|
||||||
|
export * from './gift-reply.entity';
|
||||||
|
export * from './gift.entity';
|
6
src/gift/enums/gift-color.enum.ts
Normal file
6
src/gift/enums/gift-color.enum.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export enum GiftColor {
|
||||||
|
VIOLET = 'VIOLET',
|
||||||
|
PINK = 'PINK',
|
||||||
|
BLUE = 'BLUE',
|
||||||
|
GREEN = 'GREEN',
|
||||||
|
}
|
4
src/gift/enums/gift-reply-status.enum.ts
Normal file
4
src/gift/enums/gift-reply-status.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum GiftReplyStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
OPENED = 'OPENED',
|
||||||
|
}
|
4
src/gift/enums/gift-status.enum.ts
Normal file
4
src/gift/enums/gift-status.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum GiftStatus {
|
||||||
|
AVAILABLE = 'AVAILABLE',
|
||||||
|
REDEEMED = 'REDEEMED',
|
||||||
|
}
|
3
src/gift/enums/index.ts
Normal file
3
src/gift/enums/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './gift-color.enum';
|
||||||
|
export * from './gift-reply-status.enum';
|
||||||
|
export * from './gift-status.enum';
|
14
src/gift/gift.module.ts
Normal file
14
src/gift/gift.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { JuniorModule } from '~/junior/junior.module';
|
||||||
|
import { GiftsController } from './controllers';
|
||||||
|
import { Gift, GiftRedemption, GiftReply } from './entities';
|
||||||
|
import { GiftsRepository } from './repositories';
|
||||||
|
import { GiftsService } from './services';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Gift, GiftReply, GiftRedemption]), JuniorModule],
|
||||||
|
controllers: [GiftsController],
|
||||||
|
providers: [GiftsService, GiftsRepository],
|
||||||
|
})
|
||||||
|
export class GiftModule {}
|
76
src/gift/repositories/gifts.repository.ts
Normal file
76
src/gift/repositories/gifts.repository.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { CreateGiftRequestDto, GiftFiltersRequestDto, GiftReplyRequestDto } from '../dtos/request';
|
||||||
|
import { Gift, GiftRedemption, GiftReply } from '../entities';
|
||||||
|
import { GiftStatus } from '../enums';
|
||||||
|
const ONE = 1;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GiftsRepository {
|
||||||
|
constructor(@InjectRepository(Gift) private readonly giftsRepository: Repository<Gift>) {}
|
||||||
|
|
||||||
|
create(guardianId: string, gift: CreateGiftRequestDto): Promise<Gift> {
|
||||||
|
return this.giftsRepository.save(
|
||||||
|
this.giftsRepository.create({
|
||||||
|
name: gift.name,
|
||||||
|
description: gift.description,
|
||||||
|
amount: gift.amount,
|
||||||
|
recipientId: gift.recipientId,
|
||||||
|
imageId: gift.imageId,
|
||||||
|
color: gift.color,
|
||||||
|
giverId: guardianId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
findJuniorGiftById(juniorId: string, giftId: string): Promise<Gift | null> {
|
||||||
|
return this.giftsRepository.findOne({
|
||||||
|
where: { recipientId: juniorId, id: giftId },
|
||||||
|
relations: ['recipient', 'recipient.customer', 'image', 'giver', 'giver.customer', 'reply'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findGuardianGiftById(guardianId: string, giftId: string): Promise<Gift | null> {
|
||||||
|
return this.giftsRepository.findOne({
|
||||||
|
where: { giverId: guardianId, id: giftId },
|
||||||
|
relations: ['recipient', 'recipient.customer', 'image', 'giver', 'giver.customer', 'reply'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findGifts(user: IJwtPayload, filters: GiftFiltersRequestDto) {
|
||||||
|
const query = this.giftsRepository.createQueryBuilder('gift');
|
||||||
|
if (user.roles.includes(Roles.GUARDIAN)) {
|
||||||
|
query.where('gift.giverId = :giverId', { giverId: user.sub });
|
||||||
|
} else {
|
||||||
|
query.where('gift.recipientId = :recipientId', { recipientId: user.sub });
|
||||||
|
}
|
||||||
|
query.andWhere('gift.status = :status', { status: filters.status });
|
||||||
|
query.skip((filters.page - ONE) * filters.size);
|
||||||
|
query.take(filters.size);
|
||||||
|
query.orderBy('gift.createdAt', 'DESC');
|
||||||
|
return query.getManyAndCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
redeemGift(gift: Gift) {
|
||||||
|
const redemptions = gift.redemptions || [];
|
||||||
|
gift.status = GiftStatus.REDEEMED;
|
||||||
|
gift.redeemedAt = new Date();
|
||||||
|
gift.redemptions = [...redemptions, new GiftRedemption()];
|
||||||
|
return this.giftsRepository.save(gift);
|
||||||
|
}
|
||||||
|
|
||||||
|
UndoGiftRedemption(gift: Gift) {
|
||||||
|
gift.status = GiftStatus.AVAILABLE;
|
||||||
|
gift.redeemedAt = null;
|
||||||
|
return this.giftsRepository.save(gift);
|
||||||
|
}
|
||||||
|
|
||||||
|
replyToGift(gift: Gift, reply: GiftReplyRequestDto) {
|
||||||
|
gift.reply = GiftReply.create({ ...reply });
|
||||||
|
|
||||||
|
return this.giftsRepository.save(gift);
|
||||||
|
}
|
||||||
|
}
|
1
src/gift/repositories/index.ts
Normal file
1
src/gift/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './gifts.repository';
|
101
src/gift/services/gifts.service.ts
Normal file
101
src/gift/services/gifts.service.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { Roles } from '~/auth/enums';
|
||||||
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
|
import { OciService } from '~/document/services';
|
||||||
|
import { JuniorService } from '~/junior/services';
|
||||||
|
import { CreateGiftRequestDto, GiftFiltersRequestDto, GiftReplyRequestDto } from '../dtos/request';
|
||||||
|
import { Gift } from '../entities';
|
||||||
|
import { GiftStatus } from '../enums';
|
||||||
|
import { GiftsRepository } from '../repositories';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GiftsService {
|
||||||
|
constructor(
|
||||||
|
private readonly juniorService: JuniorService,
|
||||||
|
private readonly giftsRepository: GiftsRepository,
|
||||||
|
private readonly ociService: OciService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createGift(guardianId: string, body: CreateGiftRequestDto) {
|
||||||
|
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(
|
||||||
|
guardianId,
|
||||||
|
body.recipientId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!doesJuniorBelongToGuardian) {
|
||||||
|
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const gift = await this.giftsRepository.create(guardianId, body);
|
||||||
|
|
||||||
|
return this.findUserGiftById({ sub: guardianId, roles: [Roles.GUARDIAN] }, gift.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUserGiftById(user: IJwtPayload, giftId: string) {
|
||||||
|
const gift = user.roles.includes(Roles.GUARDIAN)
|
||||||
|
? await this.giftsRepository.findGuardianGiftById(user.sub, giftId)
|
||||||
|
: await this.giftsRepository.findJuniorGiftById(user.sub, giftId);
|
||||||
|
|
||||||
|
if (!gift) {
|
||||||
|
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prepareGiftImages([gift]);
|
||||||
|
|
||||||
|
return gift;
|
||||||
|
}
|
||||||
|
|
||||||
|
async redeemGift(juniorId: string, giftId: string) {
|
||||||
|
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||||
|
|
||||||
|
if (!gift) {
|
||||||
|
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gift.status === GiftStatus.REDEEMED) {
|
||||||
|
throw new BadRequestException('GIFT.ALREADY_REDEEMED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.giftsRepository.redeemGift(gift);
|
||||||
|
}
|
||||||
|
|
||||||
|
async UndoGiftRedemption(juniorId: string, giftId: string) {
|
||||||
|
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||||
|
|
||||||
|
if (!gift) {
|
||||||
|
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gift.status === GiftStatus.AVAILABLE) {
|
||||||
|
throw new BadRequestException('GIFT.NOT_REDEEMED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.giftsRepository.UndoGiftRedemption(gift);
|
||||||
|
}
|
||||||
|
|
||||||
|
async replyToGift(juniorId: string, giftId: string, body: GiftReplyRequestDto) {
|
||||||
|
const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId);
|
||||||
|
|
||||||
|
if (!gift) {
|
||||||
|
throw new BadRequestException('GIFT.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gift.reply) {
|
||||||
|
throw new BadRequestException('GIFT.ALREADY_REPLIED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.giftsRepository.replyToGift(gift, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
findGifts(user: IJwtPayload, filters: GiftFiltersRequestDto) {
|
||||||
|
return this.giftsRepository.findGifts(user, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepareGiftImages(gifts: Gift[]) {
|
||||||
|
await Promise.all(
|
||||||
|
gifts.map(async (gift) => {
|
||||||
|
gift.image.url = await this.ociService.generatePreSignedUrl(gift.image);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
1
src/gift/services/index.ts
Normal file
1
src/gift/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './gifts.service';
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Allowance } from '~/allowance/entities';
|
import { Allowance } from '~/allowance/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
|
import { Gift } from '~/gift/entities';
|
||||||
import { Junior } from '~/junior/entities';
|
import { Junior } from '~/junior/entities';
|
||||||
import { MoneyRequest } from '~/money-request/entities';
|
import { MoneyRequest } from '~/money-request/entities';
|
||||||
import { Task } from '~/task/entities';
|
import { Task } from '~/task/entities';
|
||||||
@ -39,6 +40,9 @@ export class Guardian extends BaseEntity {
|
|||||||
@OneToMany(() => Allowance, (allowance) => allowance.guardian)
|
@OneToMany(() => Allowance, (allowance) => allowance.guardian)
|
||||||
allowances?: Allowance[];
|
allowances?: Allowance[];
|
||||||
|
|
||||||
|
@OneToMany(() => Gift, (gift) => gift.giver)
|
||||||
|
gifts?: Gift[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
import { Allowance } from '~/allowance/entities';
|
import { Allowance } from '~/allowance/entities';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { Document } from '~/document/entities';
|
import { Document } from '~/document/entities';
|
||||||
|
import { Gift } from '~/gift/entities';
|
||||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||||
import { MoneyRequest } from '~/money-request/entities';
|
import { MoneyRequest } from '~/money-request/entities';
|
||||||
import { Category, SavingGoal } from '~/saving-goals/entities';
|
import { Category, SavingGoal } from '~/saving-goals/entities';
|
||||||
@ -78,6 +79,9 @@ export class Junior extends BaseEntity {
|
|||||||
@OneToMany(() => Allowance, (allowance) => allowance.junior)
|
@OneToMany(() => Allowance, (allowance) => allowance.junior)
|
||||||
allowances!: Allowance[];
|
allowances!: Allowance[];
|
||||||
|
|
||||||
|
@OneToMany(() => Gift, (gift) => gift.recipient)
|
||||||
|
gifts!: Gift[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user