diff --git a/Contributors.md b/Contributors.md new file mode 100644 index 000000000..f907da08c --- /dev/null +++ b/Contributors.md @@ -0,0 +1,2 @@ + +Tachtwitch diff --git a/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java b/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java index 151f8b4db..8fbcdecb5 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java +++ b/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java @@ -1,5 +1,14 @@ package hng_java_boilerplate.newsletter.controller; + +import hng_java_boilerplate.exception.BadRequestException; +import hng_java_boilerplate.newsletter.dto.DeleteRequest; +import hng_java_boilerplate.newsletter.dto.SubscribeRequest; +import hng_java_boilerplate.newsletter.dto.SubscribeResponse; +import hng_java_boilerplate.newsletter.entity.Newsletter; +import hng_java_boilerplate.newsletter.service.NewsletterService; +import hng_java_boilerplate.user.dto.response.Response; + import hng_java_boilerplate.categories.dto.CategoryDto; import hng_java_boilerplate.exception.ErrorResponseDto; import hng_java_boilerplate.exception.ValidationError; @@ -13,16 +22,31 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; + import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.ResponseEnti +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/newsletter") + import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/newsletter-subscription") @Tag(name = "NewsLetter", description = "controller for newsletter") + public class NewsletterController { private final NewsletterService newsletterService; @@ -41,11 +65,34 @@ public ResponseEntity subscribe(@RequestBody @Valid Subscribe .body(newsletterService.subscribeToNewsletter(request)); } + + @GetMapping("/user/{userId}") + public ResponseEntity> getNewslettersByUserId(@PathVariable String userId, @PageableDefault(sort = "user_id", direction = Sort.Direction.DESC)Pageable pageable) { + return ResponseEntity.ok(newsletterService.findNewsletterByUserId(userId,pageable)); + } + + @GetMapping("/date/{date}") + public ResponseEntity> getNewslettersAfterDate(@PathVariable LocalDateTime date, @PageableDefault(sort = "created_at",direction = Sort.Direction.DESC)Pageable pageable) { + return ResponseEntity.ok(newsletterService.findNewsletterByCreatedAtAfter(date,pageable)); + } + + @PreAuthorize("hasRole('ROLE_SUPER_ADMIN')") + @DeleteMapping("/delete/{userId}") + public ResponseEntity deleteNewslettersById(@Valid @RequestBody DeleteRequest request) { + String user_id = request.getUser_id(); + if(user_id.isEmpty()){ + throw new BadRequestException("user id is required"); + }else { + Response response = newsletterService.deleteNewsletterByUserId(user_id); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + @GetMapping public ResponseEntity getSubscribers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { SubscribersResponse response = newsletterService.getSubscribersResponse(page, size); return ResponseEntity.ok(response); + } } diff --git a/src/main/java/hng_java_boilerplate/newsletter/dto/DeleteRequest.java b/src/main/java/hng_java_boilerplate/newsletter/dto/DeleteRequest.java new file mode 100644 index 000000000..cba800327 --- /dev/null +++ b/src/main/java/hng_java_boilerplate/newsletter/dto/DeleteRequest.java @@ -0,0 +1,10 @@ +package hng_java_boilerplate.newsletter.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DeleteRequest { + private String user_id; +} diff --git a/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java b/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java index ad87e4b65..50b9995e9 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java +++ b/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java @@ -1,5 +1,6 @@ package hng_java_boilerplate.newsletter.entity; +import hng_java_boilerplate.user.entity.User; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.CreationTimestamp; @@ -15,15 +16,26 @@ @Entity @Table(name = "newsletters") public class Newsletter { + @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + @Column(nullable = false) - private String userId; - @CreationTimestamp + private String title; + @Column(nullable = false) + private String content; + + @CreationTimestamp + @Column(name = "created_at",nullable = false) private LocalDateTime createdAt; - @Column + + @Column(name = "updated_at") @UpdateTimestamp private LocalDateTime updatedAt; } diff --git a/src/main/java/hng_java_boilerplate/newsletter/repository/NewsletterRepository.java b/src/main/java/hng_java_boilerplate/newsletter/repository/NewsletterRepository.java index f2468f39f..879628cab 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/repository/NewsletterRepository.java +++ b/src/main/java/hng_java_boilerplate/newsletter/repository/NewsletterRepository.java @@ -1,7 +1,21 @@ package hng_java_boilerplate.newsletter.repository; + import hng_java_boilerplate.newsletter.entity.Newsletter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; public interface NewsletterRepository extends JpaRepository { -} + + Page findByUser_Id(String userId, Pageable page); + + @Query("SELECT n FROM Newsletter n WHERE n.createdAt > :date") + Page findNewsletterByCreatedAtAfter(@Param("date") LocalDateTime date, Pageable page); + + void deleteByUser_Id(String userId); +} \ No newline at end of file diff --git a/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java b/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java index 35f7a91a3..4bce13a40 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java +++ b/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java @@ -7,11 +7,17 @@ import hng_java_boilerplate.newsletter.dto.SubscribersResponse; import hng_java_boilerplate.newsletter.entity.Newsletter; import hng_java_boilerplate.newsletter.repository.NewsletterRepository; +import hng_java_boilerplate.user.dto.response.Response; import hng_java_boilerplate.user.entity.User; import hng_java_boilerplate.user.repository.UserRepository; import hng_java_boilerplate.user.serviceImpl.EmailServiceImpl; import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + import org.springframework.data.domain.*; + import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -30,7 +36,7 @@ public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { .orElseThrow(() -> new NotFoundException("user not found with email")); Newsletter newsletter = new Newsletter(); - newsletter.setUserId(user.getId()); + newsletter.setUser(user); newsletter.setCreatedAt(LocalDateTime.now()); newsletter.setUpdatedAt(LocalDateTime.now()); newsletterRepository.saveAndFlush(newsletter); @@ -40,6 +46,19 @@ public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { return new SubscribeResponse(201, "subscription successful"); } + + public Page findNewsletterByUserId(String userId, Pageable pageable){ + return newsletterRepository.findByUser_Id(userId,pageable); + } + + public Page findNewsletterByCreatedAtAfter(LocalDateTime date, Pageable pageable){ + return newsletterRepository.findNewsletterByCreatedAtAfter(date,pageable); + } + + public Response deleteNewsletterByUserId(String userId){ + newsletterRepository.deleteByUser_Id(userId); + return Response.builder().status_code("success").message("Newsletter deleted successfully.").build(); + public SubscribersResponse getSubscribersResponse(int page, int size) { Pageable pageable = buildPageable(page, size); Page newsletterPage = newsletterRepository.findAll(pageable); @@ -74,5 +93,6 @@ private SubscribersResponse buildSubscribersResponse(Page newsletter .totalElements(newsletterPage.getTotalElements()) .totalPages(newsletterPage.getTotalPages()) .build(); + } } diff --git a/src/main/java/hng_java_boilerplate/user/entity/User.java b/src/main/java/hng_java_boilerplate/user/entity/User.java index 79fb26215..2afe620d4 100644 --- a/src/main/java/hng_java_boilerplate/user/entity/User.java +++ b/src/main/java/hng_java_boilerplate/user/entity/User.java @@ -1,6 +1,7 @@ package hng_java_boilerplate.user.entity; import com.fasterxml.jackson.annotation.JsonIgnore; +import hng_java_boilerplate.newsletter.entity.Newsletter; import hng_java_boilerplate.organisation.entity.Organisation; import hng_java_boilerplate.plans.entity.Plan; import hng_java_boilerplate.product.entity.Product; @@ -122,4 +123,16 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return this.isEnabled; } + + @JsonIgnore + @OneToMany(mappedBy = "user",cascade = CascadeType.ALL) + private List myNewsletters; + + @ManyToMany + @JoinTable( + name = "subscribers", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "newsletter_id") + ) + private List newsletters; } diff --git a/src/main/resources/db/migration/V49__alter_newsletter_product_table.sql b/src/main/resources/db/migration/V49__alter_newsletter_product_table.sql new file mode 100644 index 000000000..814f85feb --- /dev/null +++ b/src/main/resources/db/migration/V49__alter_newsletter_product_table.sql @@ -0,0 +1,3 @@ +ALTER TABLE newsletters +ADD COLUMN title VARCHAR(255), +ADD COLUMN content TEXT \ No newline at end of file diff --git a/src/main/resources/db/migration/V50__alter_newsletter_table.sql b/src/main/resources/db/migration/V50__alter_newsletter_table.sql new file mode 100644 index 000000000..6a6615d55 --- /dev/null +++ b/src/main/resources/db/migration/V50__alter_newsletter_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE newsletters +DROP COLUMN user_id diff --git a/src/main/resources/db/migration/V51__create_subscription_table.sql b/src/main/resources/db/migration/V51__create_subscription_table.sql new file mode 100644 index 000000000..37c5ab95c --- /dev/null +++ b/src/main/resources/db/migration/V51__create_subscription_table.sql @@ -0,0 +1,15 @@ +ALTER TABLE newsletters +ADD COLUMN user_id VARCHAR(50), +ADD CONSTRAINT fk_user_id +FOREIGN KEY (user_id) +REFERENCES users(id) +ON DELETE CASCADE +ON UPDATE CASCADE; + +CREATE TABLE subscribers( + user_id VARCHAR(50), + newsletter_id VARCHAR(50), + PRIMARY KEY (user_id, newsletter_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (newsletter_id) REFERENCES newsletters(id), + subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); \ No newline at end of file diff --git a/src/test/java/hng_java_boilerplate/newsletter/unit_test/NewsletterTest.java b/src/test/java/hng_java_boilerplate/newsletter/unit_test/NewsletterTest.java new file mode 100644 index 000000000..087a0b973 --- /dev/null +++ b/src/test/java/hng_java_boilerplate/newsletter/unit_test/NewsletterTest.java @@ -0,0 +1,100 @@ +package hng_java_boilerplate.newsletter.unit_test; + +import hng_java_boilerplate.newsletter.entity.Newsletter; +import hng_java_boilerplate.newsletter.repository.NewsletterRepository; +import hng_java_boilerplate.newsletter.service.NewsletterService; +import hng_java_boilerplate.user.dto.response.Response; +import hng_java_boilerplate.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +public class NewsletterTest { + + @InjectMocks + private NewsletterService newsletterService; + @Mock + private NewsletterRepository newsletterRepository; + + private Newsletter newsletter1; + private Newsletter newsletter2; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + User user = new User(); + user.setId("U1"); + user.setName("John Doe"); + user.setEmail("johndoe@example.com"); + user.setCreatedAt(LocalDateTime.now()); + + newsletter1 = new Newsletter(); + newsletter1.setUser(user); + newsletter1.setCreatedAt(LocalDateTime.now()); + newsletter1.setId("1"); + newsletter1.setTitle("Newsletter test"); + newsletter1.setUpdatedAt(LocalDateTime.now()); + newsletter1.setContent("this a test content for the newsletter"); + + newsletter2 = new Newsletter(); + newsletter1.setUser(user); + newsletter2.setCreatedAt(LocalDateTime.now()); + newsletter2.setId("2"); + newsletter2.setUpdatedAt(LocalDateTime.now()); + newsletter2.setTitle("Newsletter test2"); + newsletter2.setContent("this a second test content for the newsletter"); + } + + @Test + void testFindByUserId(){ + List newsletters = Arrays.asList(newsletter1,newsletter2); + Page page = new PageImpl<>(newsletters); + Pageable pageable = PageRequest.of(0,1); + + when(newsletterRepository.findByUser_Id("U1",pageable)).thenReturn(page); + + Page result = newsletterService.findNewsletterByUserId(newsletter1.getUser().getId(),pageable); + + assertNotNull(result); + assertEquals(1,result.getTotalPages()); + verify(newsletterRepository, times(1)).findByUser_Id(newsletter1.getUser().getId(),pageable); + } + + @Test + void testFindByCreatedAfter(){ + List newsletters = Arrays.asList(newsletter1,newsletter2); + Page page = new PageImpl<>(newsletters,PageRequest.of(0,1),2); + LocalDateTime date = LocalDateTime.parse("2025-02-28T11:44:32.180026100"); + when(newsletterRepository.findNewsletterByCreatedAtAfter(date,page.getPageable())).thenReturn(page); + + Page result = newsletterService.findNewsletterByCreatedAtAfter(date,page.getPageable()); + + assertNotNull(result); + verify(newsletterRepository, times(1)).findNewsletterByCreatedAtAfter(date,page.getPageable()); + } + + @Test + void testDeleteByUserId(){ + String userId = newsletter1.getUser().getId(); + + Response response = newsletterService.deleteNewsletterByUserId(userId); + + assertEquals("success", response.getStatus_code()); + assertEquals("Newsletter deleted successfully.", response.getMessage()); + verify(newsletterRepository, times(1)).deleteByUser_Id(userId); + } +}