Skip to content

Commit

Permalink
Merge pull request #1335 from darionnuel/feat/resubscribe-newsletter
Browse files Browse the repository at this point in the history
feat: Create an Endpoint to Resubscribe to Newsletter
  • Loading branch information
incredible-phoenix246 authored Mar 1, 2025
2 parents d706e90 + 9d4594c commit af961ea
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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.');
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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.' };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]' };
const mockSubscription = {
id: '1',
email: '[email protected]',
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: '[email protected]' };

jest.spyOn(repository, 'findOne').mockResolvedValue(null);

await expect(service.resubscribe(dto)).rejects.toThrow(
new NotFoundException('User not found or not unsubscribed.')
);
});
});
});

0 comments on commit af961ea

Please sign in to comment.