diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts index 9bb5a869..f8ad9711 100644 --- a/src/controllers/NewsLetterSubscriptionController.ts +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -1,7 +1,8 @@ -import { NextFunction, Request, Response } from "express"; -import { BadRequest } from "../middleware"; +import { Request, Response, NextFunction } from "express"; import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; +import { BadRequest, ResourceNotFound, Unauthorized } from "../middleware"; +// Initialize the service const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); /** @@ -86,6 +87,7 @@ const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); * type: string * example: An error occurred while processing your request. */ + const subscribeToNewsletter = async ( req: Request, res: Response, @@ -190,6 +192,129 @@ const unSubscribeToNewsletter = async ( } }; +/** + * @swagger + * /api/v1/newsletter-subscription/restore/{id}: + * post: + * summary: Restore a previously deleted newsletter subscription + * description: Allows an admin to restore a deleted newsletter subscription so that users who unsubscribed by mistake can start receiving newsletters again. + * tags: + * - Newsletter Subscription + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the deleted subscription to restore. + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Subscription successfully restored. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Subscription successfully restored. + * 400: + * description: Invalid subscription ID or request body. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Subscription ID is missing or invalid. + * 401: + * description: Unauthorized. Admin access required. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Access denied. Admins only. + * 403: + * description: Access denied due to insufficient permissions. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Access denied. Not an admin. + * 404: + * description: Subscription not found or already active. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Subscription not found or already active. + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: An error occurred while processing your request. + */ +const restoreNewsletterSubscription = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const { id: subscriptionId } = req.params; + if (!subscriptionId) { + throw new Unauthorized("Subscription ID is missing in request body."); + } + + const restoredSubscription = await newsLetterSubscriptionService.restoreSubscription(subscriptionId); + if (!restoredSubscription) { + throw new ResourceNotFound("Subscription not found or already active."); + } + + res.status(200).json({ + status: "success", + message: "Subscription successfully restored.", + }); + } catch (error) { + next(error); + } +}; + + /** * @swagger * /api/v1/newsletters: @@ -280,7 +405,7 @@ const unSubscribeToNewsletter = async ( * example: 500 */ -const getAllNewsletter = async ( + const getAllNewsletter = async ( req: Request, res: Response, next: NextFunction, @@ -304,4 +429,4 @@ const getAllNewsletter = async ( } }; -export { getAllNewsletter, subscribeToNewsletter, unSubscribeToNewsletter }; +export { getAllNewsletter, subscribeToNewsletter, unSubscribeToNewsletter, restoreNewsletterSubscription }; diff --git a/src/models/newsLetterSubscription.ts b/src/models/newsLetterSubscription.ts index a9a41829..f320779c 100644 --- a/src/models/newsLetterSubscription.ts +++ b/src/models/newsLetterSubscription.ts @@ -13,6 +13,9 @@ export class NewsLetterSubscriber { @Column() email: string; + @Column({ default: true }) + isActive: boolean; + @Column() isSubscribe: boolean; } diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts index b64af90a..0a96fa98 100644 --- a/src/routes/newsLetterSubscription.ts +++ b/src/routes/newsLetterSubscription.ts @@ -1,11 +1,11 @@ import { Router } from "express"; import { getAllNewsletter, + restoreNewsletterSubscription, subscribeToNewsletter, - unSubscribeToNewsletter, } from "../controllers/NewsLetterSubscriptionController"; import { UserRole } from "../enums/userRoles"; -import { authMiddleware, checkPermissions } from "../middleware"; +import { authMiddleware, checkPermissions, adminOnly } from "../middleware"; const newsLetterSubscriptionRoute = Router(); @@ -15,10 +15,12 @@ newsLetterSubscriptionRoute.post( subscribeToNewsletter, ); + newsLetterSubscriptionRoute.post( - "/newsletter-subscription/unsubscribe", + "/newsletter-subscription/restore/{id}", authMiddleware, - unSubscribeToNewsletter, + adminOnly, + restoreNewsletterSubscription, ); newsLetterSubscriptionRoute.get( @@ -27,4 +29,5 @@ newsLetterSubscriptionRoute.get( checkPermissions([UserRole.SUPER_ADMIN]), getAllNewsletter, ); + export { newsLetterSubscriptionRoute }; diff --git a/src/services/index.ts b/src/services/index.ts index 1826ac61..b0526ed7 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -13,4 +13,5 @@ export * from "./org.services"; export * from "./billing-plans.services"; export * from "./squeezeService"; export * from "./blogComment.services"; +export * from "./newsLetterSubscription.service"; export * from "./notification.services"; diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts index 24052042..3714b16f 100644 --- a/src/services/newsLetterSubscription.service.ts +++ b/src/services/newsLetterSubscription.service.ts @@ -2,7 +2,7 @@ import { Repository } from "typeorm"; import AppDataSource from "../data-source"; import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; import { INewsLetterSubscriptionService } from "../types"; -import { BadRequest, HttpError, ResourceNotFound } from "../middleware"; +import { HttpError, ResourceNotFound, BadRequest } from "../middleware"; export class NewsLetterSubscriptionService implements INewsLetterSubscriptionService @@ -67,6 +67,20 @@ export class NewsLetterSubscriptionService throw new BadRequest("You already unsubscribed to newsletter"); } + public async restoreSubscription(subscriptionId: string): Promise { + const subscription = await this.newsLetterSubscriber.findOne({ + where: { id: subscriptionId }, + }); + + if (!subscription || subscription.isActive) { + throw new ResourceNotFound("Subscription not found"); + } + + subscription.isActive = true; + await this.newsLetterSubscriber.save(subscription); + + return subscription; + } public async fetchAllNewsletter({ page = 1, limit = 10, diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts deleted file mode 100644 index 6f5f3e09..00000000 --- a/src/test/newsLetterSubscription.spec.ts +++ /dev/null @@ -1,239 +0,0 @@ -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, - default: { - getRepository: jest.fn(), - initialize: jest.fn(), - isInitialized: false, - }, -})); - -jest.mock("../utils", () => ({ - ...jest.requireActual("../utils"), - verifyToken: jest.fn(), -})); - -jest.mock("../models"); -jest.mock("../utils"); - -describe("NewsLetterSubscriptionService", () => { - let newsLetterSubscriptionService: NewsLetterSubscriptionService; - let newsLetterRepositoryMock: jest.Mocked>; - - beforeEach(() => { - newsLetterRepositoryMock = { - findOne: jest.fn(), - findAndCount: jest.fn(), - save: jest.fn(), - } as any; - (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { - if (entity === NewsLetterSubscriber) return newsLetterRepositoryMock; - }); - - newsLetterSubscriptionService = new NewsLetterSubscriptionService(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("SubscribeToNewsLetter", () => { - it("should subscribe a new user", async () => { - const newSubscriber = new NewsLetterSubscriber(); - newSubscriber.email = "test1@example.com"; - - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null); - (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( - (user) => { - user.id = "456"; - return Promise.resolve(user); - }, - ); - - const result = - await newsLetterSubscriptionService.subscribeUser("test1@example.com"); - - expect(result.isNewlySubscribe).toBe(true); - expect(result.subscriber).toEqual({ - id: "456", - email: "test1@example.com", - isSubscribe: true, - }); - 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) => { - user.id = "456"; - return Promise.resolve(user); - }, - ); - - const result = - await newsLetterSubscriptionService.subscribeUser("test@example.com"); - - 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"), - ); - - await expect( - newsLetterSubscriptionService.subscribeUser("test@example.com"), - ).rejects.toThrow("An error occurred while processing your request"); - }); - }); - - describe("fetchAllNewsletter", () => { - it("should fetch all newsletters with pagination", async () => { - const page = 2; - const limit = 20; - const mockSubscribers: any = [ - { id: "1", email: "user1@example.com" }, - { id: "2", email: "user2@example.com" }, - { id: "3", email: "user3@example.com" }, - ] as unknown as NewsLetterSubscriber[]; - const mockTotal = 50; - - newsLetterRepositoryMock.findAndCount.mockResolvedValue([ - mockSubscribers, - mockTotal, - ]); - - const result = await newsLetterSubscriptionService.fetchAllNewsletter({ - page, - limit, - }); - - expect(result).toEqual({ - data: mockSubscribers, - meta: { - total: mockTotal, - page, - limit, - totalPages: Math.ceil(mockTotal / limit), - }, - }); - expect(newsLetterRepositoryMock.findAndCount).toHaveBeenCalledWith({ - skip: (page - 1) * limit, - take: limit, - }); - }); - - it("should handle default pagination values", async () => { - const mockSubscribers: any = [ - { id: "1", email: "user1@example.com" }, - { id: "2", email: "user2@example.com" }, - ]; - const mockTotal = 20; - - newsLetterRepositoryMock.findAndCount.mockResolvedValue([ - mockSubscribers, - mockTotal, - ]); - - const result = await newsLetterSubscriptionService.fetchAllNewsletter({}); - - expect(result).toEqual({ - data: mockSubscribers, - meta: { - total: mockTotal, - page: 1, - limit: 10, - totalPages: 2, - }, - }); - expect(newsLetterRepositoryMock.findAndCount).toHaveBeenCalledWith({ - skip: 0, - take: 10, - }); - }); - }); - - 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); - }); - }); -});