feat: gift journeys

This commit is contained in:
Abdalhamid Alhamad
2024-12-22 16:34:02 +03:00
parent 4cef58580e
commit 5e9e83cb74
28 changed files with 656 additions and 1 deletions

View File

@ -18,6 +18,7 @@ import { buildValidationPipe } from './core/pipes';
import { CustomerModule } from './customer/customer.module';
import { migrations } from './db';
import { DocumentModule } from './document/document.module';
import { GiftModule } from './gift/gift.module';
import { GuardianModule } from './guardian/guardian.module';
import { HealthModule } from './health/health.module';
import { JuniorModule } from './junior/junior.module';
@ -61,8 +62,8 @@ import { TaskModule } from './task/task.module';
GuardianModule,
SavingGoalsModule,
AllowanceModule,
MoneyRequestModule,
GiftModule,
OtpModule,
DocumentModule,

View 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"`);
}
}

View File

@ -15,3 +15,4 @@ export * from './1734247702310-seeds-goals-categories';
export * from './1734262619426-create-junior-registration-token-table';
export * from './1734503895302-create-money-request-entity';
export * from './1734601976591-create-allowance-entities';
export * from './1734861516657-create-gift-entities';

View File

@ -1,6 +1,7 @@
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { User } from '~/auth/entities';
import { Customer } from '~/customer/entities';
import { Gift } from '~/gift/entities';
import { Junior, Theme } from '~/junior/entities';
import { SavingGoal } from '~/saving-goals/entities';
import { Task } from '~/task/entities';
@ -42,6 +43,9 @@ export class Document {
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
goals?: SavingGoal[];
@OneToMany(() => Gift, (gift) => gift.image)
gifts?: Gift[];
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;

View 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);
}
}

View File

@ -0,0 +1 @@
export * from './gifts.controller';

View 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;
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,3 @@
export * from './create-gift.request.dto';
export * from './gift-filters.request.dto';
export * from './gift-reply.request.dto';

View 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;
}
}

View 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;
}
}

View File

@ -0,0 +1,2 @@
export * from './gift-details.response.dto';
export * from './gift-list.response.dto';

View 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;
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,3 @@
export * from './gift-redemption.entity';
export * from './gift-reply.entity';
export * from './gift.entity';

View File

@ -0,0 +1,6 @@
export enum GiftColor {
VIOLET = 'VIOLET',
PINK = 'PINK',
BLUE = 'BLUE',
GREEN = 'GREEN',
}

View File

@ -0,0 +1,4 @@
export enum GiftReplyStatus {
PENDING = 'PENDING',
OPENED = 'OPENED',
}

View File

@ -0,0 +1,4 @@
export enum GiftStatus {
AVAILABLE = 'AVAILABLE',
REDEEMED = 'REDEEMED',
}

3
src/gift/enums/index.ts Normal file
View 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
View 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 {}

View 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);
}
}

View File

@ -0,0 +1 @@
export * from './gifts.repository';

View 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);
}),
);
}
}

View File

@ -0,0 +1 @@
export * from './gifts.service';

View File

@ -11,6 +11,7 @@ import {
} from 'typeorm';
import { Allowance } from '~/allowance/entities';
import { Customer } from '~/customer/entities';
import { Gift } from '~/gift/entities';
import { Junior } from '~/junior/entities';
import { MoneyRequest } from '~/money-request/entities';
import { Task } from '~/task/entities';
@ -39,6 +40,9 @@ export class Guardian extends BaseEntity {
@OneToMany(() => Allowance, (allowance) => allowance.guardian)
allowances?: Allowance[];
@OneToMany(() => Gift, (gift) => gift.giver)
gifts?: Gift[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;

View File

@ -13,6 +13,7 @@ import {
import { Allowance } from '~/allowance/entities';
import { Customer } from '~/customer/entities';
import { Document } from '~/document/entities';
import { Gift } from '~/gift/entities';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { MoneyRequest } from '~/money-request/entities';
import { Category, SavingGoal } from '~/saving-goals/entities';
@ -78,6 +79,9 @@ export class Junior extends BaseEntity {
@OneToMany(() => Allowance, (allowance) => allowance.junior)
allowances!: Allowance[];
@OneToMany(() => Gift, (gift) => gift.recipient)
gifts!: Gift[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;