diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts index b4bf127a..9bb5a869 100644 --- a/src/controllers/NewsLetterSubscriptionController.ts +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -11,7 +11,7 @@ const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); * summary: Subscribe to the newsletter * description: Allows a user to subscribe to the newsletter by providing an email address. * tags: - * - Newsletter Subscription + * - Newsletter * requestBody: * required: true * content: @@ -52,6 +52,23 @@ const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); * type: string * example: You are already subscribed to our newsletter. * + * 400: + * description: User is already subscribed but unsubscribe. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: false + * status_code: + * type: number + * example: 400 + * message: + * type: string + * example: You are already subscribed, please enable newsletter subscription to receive newsletter again + * * 500: * description: Internal server error. An error occurred while processing the subscription. * content: @@ -80,12 +97,94 @@ const subscribeToNewsletter = async ( throw new BadRequest("Email is missing in request body."); } const subscriber = await newsLetterSubscriptionService.subscribeUser(email); - res.status(!subscriber.isSubscribe ? 201 : 200).json({ + res.status(subscriber.isNewlySubscribe ? 201 : 200).json({ status: "success", - message: !subscriber.isSubscribe + message: subscriber.isNewlySubscribe ? "Subscriber subscription successful" : "You are already subscribed to our newsletter", }); + } catch (error) { + console.log(error); + + next(error); + } +}; + +/** + * @swagger + * /newsletter/unsubscribe: + * post: + * summary: Unsubscribe from newsletter + * description: Allows a logedegin user to unsubscribe from the newsletter using their email address. + * tags: + * - Newsletter + * security: + * - bearerAuth: [] # Assumes you're using bearer token authentication + * responses: + * 200: + * description: Successfully unsubscribed from the newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Successfully unsubscribed from newsletter + * 400: + * description: Bad request, missing or invalid email. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: number + * example: 400 + * message: + * type: string + * example: You already unsubscribed to newsletter. + * 404: + * description: User not subscribed ti newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: number + * example: 404 + * message: + * type: string + * example: You are not subscribed to newsletter. + */ +const unSubscribeToNewsletter = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { email } = req.user; + if (!email) { + throw new BadRequest("Email is missing in request body."); + } + const subscriber = + await newsLetterSubscriptionService.unSubcribeUser(email); + if (subscriber) { + res.status(200).json({ + status: "success", + message: "Successfully unsubscribed from newsletter", + }); + } } catch (error) { next(error); } @@ -205,4 +304,4 @@ const getAllNewsletter = async ( } }; -export { getAllNewsletter, subscribeToNewsletter }; +export { getAllNewsletter, subscribeToNewsletter, unSubscribeToNewsletter }; diff --git a/src/models/newsLetterSubscription.ts b/src/models/newsLetterSubscription.ts index 8f83c605..a9a41829 100644 --- a/src/models/newsLetterSubscription.ts +++ b/src/models/newsLetterSubscription.ts @@ -12,4 +12,7 @@ export class NewsLetterSubscriber { @Column() email: string; + + @Column() + isSubscribe: boolean; } diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts index 8e2773dd..b64af90a 100644 --- a/src/routes/newsLetterSubscription.ts +++ b/src/routes/newsLetterSubscription.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import { getAllNewsletter, subscribeToNewsletter, + unSubscribeToNewsletter, } from "../controllers/NewsLetterSubscriptionController"; import { UserRole } from "../enums/userRoles"; import { authMiddleware, checkPermissions } from "../middleware"; @@ -14,6 +15,12 @@ newsLetterSubscriptionRoute.post( subscribeToNewsletter, ); +newsLetterSubscriptionRoute.post( + "/newsletter-subscription/unsubscribe", + authMiddleware, + unSubscribeToNewsletter, +); + newsLetterSubscriptionRoute.get( "/newsletter-subscription", authMiddleware, diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts index 9e8e2f9a..24052042 100644 --- a/src/services/newsLetterSubscription.service.ts +++ b/src/services/newsLetterSubscription.service.ts @@ -1,8 +1,8 @@ import { Repository } from "typeorm"; import AppDataSource from "../data-source"; -import { HttpError } from "../middleware"; import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; import { INewsLetterSubscriptionService } from "../types"; +import { BadRequest, HttpError, ResourceNotFound } from "../middleware"; export class NewsLetterSubscriptionService implements INewsLetterSubscriptionService @@ -15,27 +15,56 @@ export class NewsLetterSubscriptionService } public async subscribeUser(email: string): Promise<{ - isSubscribe: boolean; + isNewlySubscribe: boolean; subscriber: NewsLetterSubscriber; }> { - let isSubscribe = false; + let isNewlySubscribe = true; + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ where: { email }, }); - if (isExistingSubscriber) { - isSubscribe = true; - return { isSubscribe, subscriber: isExistingSubscriber }; + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { + isNewlySubscribe = false; + return { isNewlySubscribe, subscriber: isExistingSubscriber }; } + + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === false) { + throw new BadRequest( + "You are already subscribed, please enable newsletter subscription to receive newsletter again", + ); + } + const newSubscriber = new NewsLetterSubscriber(); newSubscriber.email = email; + newSubscriber.isSubscribe = true; + const subscriber = await this.newsLetterSubscriber.save(newSubscriber); + if (!subscriber) { throw new HttpError( 500, "An error occurred while processing your request", ); } - return { isSubscribe, subscriber }; + return { isNewlySubscribe, subscriber }; + } + + public async unSubcribeUser(email: string): Promise { + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ + where: { email }, + }); + + if (!isExistingSubscriber) { + throw new ResourceNotFound("You are not subscribed to newsletter"); + } + + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { + isExistingSubscriber.isSubscribe = false; + await this.newsLetterSubscriber.save(isExistingSubscriber); + return isExistingSubscriber; + } + + throw new BadRequest("You already unsubscribed to newsletter"); } public async fetchAllNewsletter({ diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts index 34201227..6f5f3e09 100644 --- a/src/test/newsLetterSubscription.spec.ts +++ b/src/test/newsLetterSubscription.spec.ts @@ -2,6 +2,7 @@ import { Repository } from "typeorm"; import AppDataSource from "../data-source"; import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; +import { BadRequest, ResourceNotFound } from "../middleware"; jest.mock("../data-source", () => ({ __esModule: true, @@ -22,9 +23,7 @@ jest.mock("../utils"); describe("NewsLetterSubscriptionService", () => { let newsLetterSubscriptionService: NewsLetterSubscriptionService; - let newsLetterRepositoryMock: jest.Mocked< - Repository - >; + let newsLetterRepositoryMock: jest.Mocked>; beforeEach(() => { newsLetterRepositoryMock = { @@ -45,12 +44,8 @@ describe("NewsLetterSubscriptionService", () => { describe("SubscribeToNewsLetter", () => { it("should subscribe a new user", async () => { - const user = new NewsLetterSubscriber(); - user.email = "test@example.com"; - - const payload = { - email: "test1@example.com", - }; + const newSubscriber = new NewsLetterSubscriber(); + newSubscriber.email = "test1@example.com"; (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null); (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( @@ -59,21 +54,29 @@ describe("NewsLetterSubscriptionService", () => { return Promise.resolve(user); }, ); + const result = await newsLetterSubscriptionService.subscribeUser("test1@example.com"); - expect(result.isSubscribe).toBe(false); + expect(result.isNewlySubscribe).toBe(true); expect(result.subscriber).toEqual({ id: "456", email: "test1@example.com", + isSubscribe: true, }); - expect(newsLetterRepositoryMock.save).toHaveBeenCalled(); + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + email: "test1@example.com", + isSubscribe: true, + }), + ); }); it("should handle already subscribed user", async () => { const user = new NewsLetterSubscriber(); user.id = "123"; user.email = "test@example.com"; + user.isSubscribe = true; (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( (user) => { @@ -81,17 +84,33 @@ describe("NewsLetterSubscriptionService", () => { return Promise.resolve(user); }, ); + const result = await newsLetterSubscriptionService.subscribeUser("test@example.com"); - expect(result.isSubscribe).toBe(true); + expect(result.isNewlySubscribe).toBe(false); expect(result.subscriber).toEqual({ id: "123", email: "test@example.com", + isSubscribe: true, }); expect(newsLetterRepositoryMock.save).not.toHaveBeenCalled(); }); + it("should throw a Conflict error if already subscribed but inactive", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + it("should throw an error if something goes wrong", async () => { (newsLetterRepositoryMock.findOne as jest.Mock).mockRejectedValue( new Error("An error occurred while processing your request"), @@ -168,4 +187,53 @@ describe("NewsLetterSubscriptionService", () => { }); }); }); + + describe("UnsubscribeFromNewsLetter", () => { + it("should successfully unsubscribe a logged-in user from the newsletter", async () => { + const user = new NewsLetterSubscriber(); + user.email = "test1@example.com"; + user.id = "5678"; + user.isSubscribe = true; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); + + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.isSubscribe = false; + return Promise.resolve(user); + }, + ); + + const result = + await newsLetterSubscriptionService.unSubcribeUser("test1@example.com"); + + expect(result).toEqual({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }); + + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }), + ); + }); + + it("should throw an error if user is not subscribed", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + }); }); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ab301391..4127a7ab 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -87,6 +87,7 @@ export interface GoogleUser { export interface INewsLetterSubscriptionService { subscribeUser(email: string): Promise; + unSubcribeUser(email: string): Promise; } export interface INewsLetterSubscription {