diff --git a/src/modules/newsletter-subscription/dto/resubscribe-newsletter.dto.ts b/src/modules/newsletter-subscription/dto/resubscribe-newsletter.dto.ts new file mode 100644 index 000000000..5ea5620ae --- /dev/null +++ b/src/modules/newsletter-subscription/dto/resubscribe-newsletter.dto.ts @@ -0,0 +1,18 @@ +import { IsEmail, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; + +export class ResubscribeNewsletterDto { + @IsOptional() + @IsUUID() + id?: string; + + @IsOptional() + @IsEmail() + email?: string; + + @IsNotEmpty() + validateUserIdentifier?() { + if (!this.id && !this.email) { + throw new Error('Either userId or email must be provided.'); + } + } +} diff --git a/src/modules/newsletter-subscription/entities/newsletter-subscription.entity.ts b/src/modules/newsletter-subscription/entities/newsletter-subscription.entity.ts index 3ec042d94..4c07fa558 100644 --- a/src/modules/newsletter-subscription/entities/newsletter-subscription.entity.ts +++ b/src/modules/newsletter-subscription/entities/newsletter-subscription.entity.ts @@ -1,14 +1,26 @@ import { AbstractBaseEntity } from '../../../entities/base.entity'; -import { Column, DeleteDateColumn, Entity } from 'typeorm'; +import { Column, DeleteDateColumn, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity('newsletters') export class NewsletterSubscription extends AbstractBaseEntity { - @Column({ unique: true }) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true, nullable: true }) email: string; + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + @DeleteDateColumn() deletedAt: Date; @Column({ default: false }) isUnsubscribed: boolean; + + @Column({ default: 'inactive' }) // inactive, active, unsubscribed + status: string; } diff --git a/src/modules/newsletter-subscription/newsletter-subscription.controller.ts b/src/modules/newsletter-subscription/newsletter-subscription.controller.ts index 2cfcb2f66..5a56ceba0 100644 --- a/src/modules/newsletter-subscription/newsletter-subscription.controller.ts +++ b/src/modules/newsletter-subscription/newsletter-subscription.controller.ts @@ -6,6 +6,7 @@ import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@ne import { NewsletterSubscriptionResponseDto } from './dto/newsletter-subscription.response.dto'; import { SuperAdminGuard } from '@guards/super-admin.guard'; import { UnsubscribeNewsletterDto } from './dto/unsubscribe-newsletter.dto'; +import { ResubscribeNewsletterDto } from './dto/resubscribe-newsletter.dto'; @ApiTags('Newsletter Subscription') @Controller('newsletter-subscription') @@ -21,6 +22,17 @@ export class NewsletterSubscriptionController { return this.newsletterSubscriptionService.newsletterSubscription(createNewsletterDto); } + @skipAuth() + @Post('resubscribe') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Resubscribe to the newsletter' }) + @ApiResponse({ status: 200, description: 'User successfully resubscribed.' }) + @ApiResponse({ status: 400, description: 'User is already subscribed.' }) + @ApiResponse({ status: 404, description: 'User not found or not unsubscribed.' }) + resubscribe(@Body() resubscribeDto: ResubscribeNewsletterDto) { + return this.newsletterSubscriptionService.resubscribe(resubscribeDto); + } + @ApiBearerAuth() @UseGuards(SuperAdminGuard) @Get() diff --git a/src/modules/newsletter-subscription/newsletter-subscription.service.ts b/src/modules/newsletter-subscription/newsletter-subscription.service.ts index cf44397d0..982486daf 100644 --- a/src/modules/newsletter-subscription/newsletter-subscription.service.ts +++ b/src/modules/newsletter-subscription/newsletter-subscription.service.ts @@ -1,9 +1,10 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not, Repository } from 'typeorm'; import { CreateNewsletterSubscriptionDto } from './dto/create-newsletter-subscription.dto'; import { NewsletterSubscriptionResponseDto } from './dto/newsletter-subscription.response.dto'; import { NewsletterSubscription } from './entities/newsletter-subscription.entity'; +import { ResubscribeNewsletterDto } from './dto/resubscribe-newsletter.dto'; @Injectable() export class NewsletterSubscriptionService { @@ -85,4 +86,27 @@ export class NewsletterSubscriptionService { return { message: `Email ${email} has been unsubscribed successfully` }; } + async resubscribe(dto: ResubscribeNewsletterDto): Promise<{ message: string }> { + const { id, email } = dto; + + // Find the subscription record + const userSubscription = await this.newsletterSubscriptionRepository.findOne({ + where: [{ id }, { email }], + }); + + if (!userSubscription) { + throw new NotFoundException('User not found or not unsubscribed.'); + } + + // Check if the user is already subscribed + if (userSubscription.status === 'active') { + throw new BadRequestException('User is already subscribed.'); + } + + // Update the subscription status + userSubscription.status = 'active'; + await this.newsletterSubscriptionRepository.save(userSubscription); + + return { message: 'Successfully resubscribed to the newsletter.' }; + } } diff --git a/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts b/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts index f7e05a744..87a11818c 100644 --- a/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts +++ b/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts @@ -189,4 +189,38 @@ describe('NewsletterService', () => { expect(repository.findOne).toHaveBeenCalledWith({ where: { email } }); }); }); + + describe('resubscribe', () => { + it('should successfully resubscribe a user who was unsubscribed', async () => { + const dto = { email: 'test@example.com' }; + const mockSubscription = { + id: '1', + email: 'test@example.com', + status: 'unsubscribed', + } as NewsletterSubscription; + + jest.spyOn(repository, 'findOne').mockResolvedValue(mockSubscription); + jest.spyOn(repository, 'save').mockResolvedValue({ ...mockSubscription, status: 'active' }); + + const result = await service.resubscribe(dto); + + expect(result).toEqual({ message: 'Successfully resubscribed to the newsletter.' }); + expect(repository.findOne).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.arrayContaining([expect.objectContaining({ email: dto.email })]), + }) + ); + expect(repository.save).toHaveBeenCalledWith(expect.objectContaining({ status: 'active' })); + }); + + it('should throw NotFoundException if user is not found or was never subscribed', async () => { + const dto = { email: 'notfound@example.com' }; + + jest.spyOn(repository, 'findOne').mockResolvedValue(null); + + await expect(service.resubscribe(dto)).rejects.toThrow( + new NotFoundException('User not found or not unsubscribed.') + ); + }); + }); });