From 072af1ac0586e291a338b6bbb71cc804f16c2e80 Mon Sep 17 00:00:00 2001 From: mtrznadel24 Date: Thu, 7 May 2026 19:50:55 +0200 Subject: [PATCH 1/6] feature(notification): create notification entity and enum for types. --- .../domain/entity/notification/Notification.java | 4 ++++ .../domain/enums/NotificationType.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/enums/NotificationType.java diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java new file mode 100644 index 00000000..20df15e8 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java @@ -0,0 +1,4 @@ +package pl.edu.agh.project_manager.domain.entity.notification; + +public class Notification { +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/enums/NotificationType.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/enums/NotificationType.java new file mode 100644 index 00000000..bcfb8d0f --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/enums/NotificationType.java @@ -0,0 +1,15 @@ +package pl.edu.agh.project_manager.domain.enums; + +public enum NotificationType { + + ASSIGNMENT_REQUESTED, + ASSIGNMENT_ACCEPTED, + ASSIGNMENT_REJECTED, + + QUALIFICATION_REQUESTED, + QUALIFICATION_ACCEPTED, + QUALIFICATION_REJECTED, + + SYSTEM_NEW_EMPLOYEE + +} \ No newline at end of file From 676ae811fdfd5022cf335cdf60960bd61e8cb3df Mon Sep 17 00:00:00 2001 From: mtrznadel24 Date: Thu, 7 May 2026 21:46:27 +0200 Subject: [PATCH 2/6] feature(notification): fill in Notification entity, create event records, notificationRepository and NotificationService with methods --- .../entity/notification/Notification.java | 47 ++++++++++- .../domain/entity/user/User.java | 5 ++ .../domain/event/AssignmentAcceptedEvent.java | 11 +++ .../event/AssignmentRequestedEvent.java | 11 +++ .../event/QualificationRequestedEvent.java | 11 +++ .../domain/exception/ApiErrorCode.java | 2 + .../notification/NotificationRepository.java | 21 +++++ .../notification/NotificationService.java | 80 +++++++++++++++++++ 8 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java index 20df15e8..987df093 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/notification/Notification.java @@ -1,4 +1,49 @@ package pl.edu.agh.project_manager.domain.entity.notification; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "notifications", indexes = { + @Index(name = "idx_notification_recipient_created", columnList = "recipient_id, created_at") +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder public class Notification { -} + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id", nullable = false) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "recipient_id", nullable = false) + private User recipient; + + @Enumerated(EnumType.STRING) + @Column(name = "type", length = 50, nullable = false) + private NotificationType type; + + @Column(name = "message", length = 255, nullable = false) + private String message; + + @Column(name = "is_read", nullable = false) + @Builder.Default + private boolean isRead = false; + + @Column(name = "reference_id") + private UUID referenceId; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java index c4f21694..111c4783 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import pl.edu.agh.project_manager.domain.entity.notification.Notification; import pl.edu.agh.project_manager.domain.entity.project.Project; import pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup; import pl.edu.agh.project_manager.domain.enums.UserRole; @@ -67,6 +68,10 @@ public class User { @Builder.Default private List qualifications = new ArrayList<>(); + @OneToMany(mappedBy = "recipient", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List notifications = new ArrayList<>(); + public void addQualification(Qualification qualification) { qualifications.add(qualification); qualification.setUser(this); diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java new file mode 100644 index 00000000..26e61c16 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record AssignmentAcceptedEvent( + UUID assignmentId, + User recipient, String + message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java new file mode 100644 index 00000000..66ffb6e5 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record AssignmentRequestedEvent( + UUID assignmentId, + User recipient, + String message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java new file mode 100644 index 00000000..370e3a3d --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record QualificationRequestedEvent( + UUID qualificationId, + User recipient, + String message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java index b5aa4cbd..22d6412a 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/exception/ApiErrorCode.java @@ -32,6 +32,8 @@ public enum ApiErrorCode { QUALIFICATION_NOT_FOUND("QUAL_001", HttpStatus.NOT_FOUND, "Qualification not found"), + NOTIFICATION_NOT_FOUND("NOTIF_001", HttpStatus.NOT_FOUND, "Notification not found"), + ROLE_NOT_IN_PROJECT("REQ_001", HttpStatus.BAD_REQUEST, "Role must exist within the provided project"), USER_ALREADY_IN_PROJECT("REQ_002", HttpStatus.CONFLICT, "User is already a member of this project"), USER_HAS_ONGOING_REQUEST("REQ_003", HttpStatus.CONFLICT, "User already has a pending request for this project"), diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java new file mode 100644 index 00000000..f4404797 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java @@ -0,0 +1,21 @@ +package pl.edu.agh.project_manager.repository.notification; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import pl.edu.agh.project_manager.domain.entity.notification.Notification; + +import java.util.List; +import java.util.UUID; + +public interface NotificationRepository extends JpaRepository { + + List findAllByRecipientIdOrderByCreatedAtDesc(UUID recipientId); + + List findTop50ByRecipientIdOrderByCreatedAtDesc(UUID recipientId); + + @Modifying + @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :userId AND n.isRead = false") + void markAllAsReadByUserId(@Param("userId") UUID userId); +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java new file mode 100644 index 00000000..05ff9c24 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java @@ -0,0 +1,80 @@ +package pl.edu.agh.project_manager.service.notification; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import pl.edu.agh.project_manager.domain.entity.notification.Notification; +import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; +import pl.edu.agh.project_manager.domain.event.AssignmentAcceptedEvent; +import pl.edu.agh.project_manager.domain.event.AssignmentRequestedEvent; +import pl.edu.agh.project_manager.domain.event.QualificationRequestedEvent; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; +import pl.edu.agh.project_manager.repository.notification.NotificationRepository; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class NotificationService { + + private final NotificationRepository notificationRepository; + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAssignmentRequested(AssignmentRequestedEvent event) { + create(event.recipient(), NotificationType.ASSIGNMENT_REQUESTED, event.message(), event.assignmentId()); + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onQualificationRequested(QualificationRequestedEvent event) { + create(event.recipient(), NotificationType.QUALIFICATION_REQUESTED, event.message(), event.qualificationId()); + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAssignmentAccepted(AssignmentAcceptedEvent event) { + create(event.recipient(), NotificationType.ASSIGNMENT_ACCEPTED, event.message(), event.assignmentId()); + } + + private void create(User recipient, NotificationType type, String message, UUID referenceId) { + Notification notification = Notification.builder() + .recipient(recipient) + .type(type) + .message(message) + .referenceId(referenceId) + .build(); + notificationRepository.save(notification); + } + + @Transactional(readOnly = true) + public List getAllForUser(UUID userId) { + return notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId); + } + + @Transactional(readOnly = true) + public List getFirst50ForUser(UUID userId) { + return notificationRepository.findTop50ByRecipientIdOrderByCreatedAtDesc(userId); + } + + @Transactional + public void markAsRead(UUID notificationId, UUID userId) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new ApplicationException( + ApiErrorCode.NOTIFICATION_NOT_FOUND, + "Cannot find notification: " + notificationId) + ); + + notification.setRead(true); + } + + @Transactional + public void markAllAsRead(UUID userId) { + notificationRepository.markAllAsReadByUserId(userId); + } +} \ No newline at end of file From fcf521a8833325295a3a7ef50e7bc8fdcc68100f Mon Sep 17 00:00:00 2001 From: mtrznadel24 Date: Thu, 7 May 2026 22:47:03 +0200 Subject: [PATCH 3/6] feature(notification): Create event records and NotificationController with methods and create notification access. --- .../notification/NotificationResponse.java | 27 ++++++++++ .../notification/NotificationController.java | 50 +++++++++++++++++++ .../domain/event/AssignmentRejectedEvent.java | 11 ++++ .../event/QualificationAcceptedEvent.java | 11 ++++ .../event/QualificationRejectedEvent.java | 11 ++++ .../domain/event/SystemNewEmployeeEvent.java | 11 ++++ .../notification/NotificationRepository.java | 5 +- .../security/access/NotificationAccess.java | 25 ++++++++++ .../notification/NotificationService.java | 47 +++++++++++++---- 9 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/notification/NotificationResponse.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/NotificationAccess.java diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/notification/NotificationResponse.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/notification/NotificationResponse.java new file mode 100644 index 00000000..c0e5e6af --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/notification/NotificationResponse.java @@ -0,0 +1,27 @@ +package pl.edu.agh.project_manager.controller.dto.notification; + +import pl.edu.agh.project_manager.domain.entity.notification.Notification; +import pl.edu.agh.project_manager.domain.enums.NotificationType; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record NotificationResponse( + UUID id, + NotificationType type, + String message, + boolean isRead, + UUID referenceId, + LocalDateTime createdAt +) { + public static NotificationResponse from(Notification notification) { + return new NotificationResponse( + notification.getId(), + notification.getType(), + notification.getMessage(), + notification.isRead(), + notification.getReferenceId(), + notification.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java new file mode 100644 index 00000000..2f3e0295 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java @@ -0,0 +1,50 @@ +package pl.edu.agh.project_manager.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import pl.edu.agh.project_manager.controller.dto.notification.NotificationResponse; +import pl.edu.agh.project_manager.domain.entity.notification.Notification; +import pl.edu.agh.project_manager.security.UserPrincipal; +import pl.edu.agh.project_manager.service.notification.NotificationService; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +public class NotificationController { + + private final NotificationService notificationService; + + @GetMapping + public ResponseEntity> getUserNotifications( + @RequestParam(defaultValue = "50") int limit, + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + return ResponseEntity.ok(notificationService.getNotificationsForUser(userPrincipal.userId(), limit)); + } + + @PatchMapping("/{notificationId}/mark-as-read") + @PreAuthorize("@notificationAccess.canMarkAsRead(#notificationId, authentication.principal)") + public ResponseEntity markAsRead( + @PathVariable UUID notificationId, + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + notificationService.markAsRead(notificationId, userPrincipal.userId()); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/mark-all-as-read") + public ResponseEntity markAllAsRead( + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + notificationService.markAllAsRead(userPrincipal.userId()); + return ResponseEntity.noContent().build(); + } + +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java new file mode 100644 index 00000000..57d367a4 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record AssignmentRejectedEvent( + UUID assignmentId, + User recipient, + String message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java new file mode 100644 index 00000000..ae4e3b22 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record QualificationAcceptedEvent( + UUID qualificationId, + User recipient, + String message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java new file mode 100644 index 00000000..2b5dda34 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record QualificationRejectedEvent( + UUID qualificationId, + User recipient, + String message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java new file mode 100644 index 00000000..fb7db3c2 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.UUID; + +public record SystemNewEmployeeEvent( + UUID employeeId, + User recipient, + String message +) {} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java index f4404797..c201a6d7 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java @@ -1,5 +1,6 @@ package pl.edu.agh.project_manager.repository.notification; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -11,9 +12,9 @@ public interface NotificationRepository extends JpaRepository { - List findAllByRecipientIdOrderByCreatedAtDesc(UUID recipientId); + boolean existsByIdAndRecipientId(UUID notificationId, UUID recipientId); - List findTop50ByRecipientIdOrderByCreatedAtDesc(UUID recipientId); + List findAllByRecipientIdOrderByCreatedAtDesc(UUID recipientId, Pageable pageable); @Modifying @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :userId AND n.isRead = false") diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/NotificationAccess.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/NotificationAccess.java new file mode 100644 index 00000000..1fe8f85f --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/security/access/NotificationAccess.java @@ -0,0 +1,25 @@ +package pl.edu.agh.project_manager.security.access; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import pl.edu.agh.project_manager.repository.notification.NotificationRepository; +import pl.edu.agh.project_manager.security.UserPrincipal; + +import java.util.UUID; + +@Component("notificationAccess") +@RequiredArgsConstructor +public class NotificationAccess { + + private final NotificationRepository notificationRepository; + + public boolean canMarkAsRead(UUID notificationId, UserPrincipal currentUser) { + if (currentUser == null) { + return false; + } + + UUID userId = currentUser.userId(); + + return notificationRepository.existsByIdAndRecipientId(notificationId, userId); + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java index 05ff9c24..b48401ca 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java @@ -1,16 +1,17 @@ package pl.edu.agh.project_manager.service.notification; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; +import pl.edu.agh.project_manager.controller.dto.notification.NotificationResponse; import pl.edu.agh.project_manager.domain.entity.notification.Notification; import pl.edu.agh.project_manager.domain.entity.user.User; import pl.edu.agh.project_manager.domain.enums.NotificationType; -import pl.edu.agh.project_manager.domain.event.AssignmentAcceptedEvent; -import pl.edu.agh.project_manager.domain.event.AssignmentRequestedEvent; -import pl.edu.agh.project_manager.domain.event.QualificationRequestedEvent; +import pl.edu.agh.project_manager.domain.event.*; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.repository.notification.NotificationRepository; @@ -30,6 +31,18 @@ public void onAssignmentRequested(AssignmentRequestedEvent event) { create(event.recipient(), NotificationType.ASSIGNMENT_REQUESTED, event.message(), event.assignmentId()); } + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAssignmentAccepted(AssignmentAcceptedEvent event) { + create(event.recipient(), NotificationType.ASSIGNMENT_ACCEPTED, event.message(), event.assignmentId()); + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAssignmentRejected(AssignmentRejectedEvent event) { + create(event.recipient(), NotificationType.ASSIGNMENT_REJECTED, event.message(), event.assignmentId()); + } + @TransactionalEventListener @Transactional(propagation = Propagation.REQUIRES_NEW) public void onQualificationRequested(QualificationRequestedEvent event) { @@ -38,8 +51,20 @@ public void onQualificationRequested(QualificationRequestedEvent event) { @TransactionalEventListener @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onAssignmentAccepted(AssignmentAcceptedEvent event) { - create(event.recipient(), NotificationType.ASSIGNMENT_ACCEPTED, event.message(), event.assignmentId()); + public void onQualificationAccepted(QualificationAcceptedEvent event) { + create(event.recipient(), NotificationType.QUALIFICATION_ACCEPTED, event.message(), event.qualificationId()); + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onQualificationRejected(QualificationRejectedEvent event) { + create(event.recipient(), NotificationType.QUALIFICATION_REJECTED, event.message(), event.qualificationId()); + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onSystemNewEmployee(SystemNewEmployeeEvent event) { + create(event.recipient(), NotificationType.SYSTEM_NEW_EMPLOYEE, event.message(), event.employeeId()); } private void create(User recipient, NotificationType type, String message, UUID referenceId) { @@ -53,13 +78,13 @@ private void create(User recipient, NotificationType type, String message, UUID } @Transactional(readOnly = true) - public List getAllForUser(UUID userId) { - return notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId); - } + public List getNotificationsForUser(UUID userId, int limit) { + Pageable pageable = PageRequest.of(0, limit); - @Transactional(readOnly = true) - public List getFirst50ForUser(UUID userId) { - return notificationRepository.findTop50ByRecipientIdOrderByCreatedAtDesc(userId); + return notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId, pageable) + .stream() + .map(NotificationResponse::from) + .toList(); } @Transactional From 0772ce41c483adc3e67f1d79f87397501505094c Mon Sep 17 00:00:00 2001 From: mtrznadel24 Date: Thu, 7 May 2026 23:14:34 +0200 Subject: [PATCH 4/6] feature(notification): Implement ApplicationEventPublisher in other services --- .../domain/entity/user/User.java | 4 +++ .../project/ProjectAssignmentService.java | 25 +++++++++++++++++++ .../service/user/QualificationService.java | 16 +++++++++++- .../service/user/UserInvitationService.java | 11 ++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java index 111c4783..e1dda77c 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/entity/user/User.java @@ -76,4 +76,8 @@ public void addQualification(Qualification qualification) { qualifications.add(qualification); qualification.setUser(this); } + + public String getFullName() { + return String.format("%s %s", this.name, this.surname); + } } \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java index 3a6a0f40..4738f358 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.service.project; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import pl.edu.agh.project_manager.controller.dto.project.AssignmentResponse; @@ -8,6 +9,9 @@ import pl.edu.agh.project_manager.domain.entity.user.User; import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment; import pl.edu.agh.project_manager.domain.enums.AssignmentStatus; +import pl.edu.agh.project_manager.domain.event.AssignmentAcceptedEvent; +import pl.edu.agh.project_manager.domain.event.AssignmentRejectedEvent; +import pl.edu.agh.project_manager.domain.event.AssignmentRequestedEvent; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.repository.project.ProjectAssignmentRepository; @@ -25,6 +29,7 @@ public class ProjectAssignmentService { private final ProjectService projectService; private final UserService userService; private final ProjectAssignmentRepository assignmentRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public AssignmentResponse createAssignment(UUID projectId, AssignmentCommand command) { @@ -45,6 +50,14 @@ public AssignmentResponse createAssignment(UUID projectId, AssignmentCommand com ProjectAssignment savedAssignment = assignmentRepository.save(assignment); + if (user.getSupervisor() != null) { + eventPublisher.publishEvent(new AssignmentRequestedEvent( + savedAssignment.getId(), + user.getSupervisor(), + "Kierownik projektu " + project.getTitle() + " prosi o alokację pracownika " + user.getName() + " " + user.getSurname() + )); + } + return AssignmentResponse.from(savedAssignment); } @@ -108,6 +121,12 @@ public void acceptAssignment(UUID assignmentId) { if (!isAlreadyMember) { project.addMember(user); } + + eventPublisher.publishEvent(new AssignmentAcceptedEvent( + assignment.getId(), + project.getProjectManager(), + "Zaakceptowano przypisanie pracownika " + user.getName() + " do projektu " + project.getTitle() + )); } @Transactional @@ -117,6 +136,12 @@ public void rejectAssignment(UUID assignmentId) { validatePendingStatus(assignment); assignment.setStatus(AssignmentStatus.REJECTED); + + eventPublisher.publishEvent(new AssignmentRejectedEvent( + assignment.getId(), + assignment.getProject().getProjectManager(), + "Odrzucono przypisanie pracownika " + assignment.getUser().getName() + " do projektu " + assignment.getProject().getTitle() + )); } public ProjectAssignment getAssignmentOrThrow(UUID assignmentId) { diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/QualificationService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/QualificationService.java index f523ec1f..db8dc756 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/QualificationService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/QualificationService.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.service.user; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,6 +11,7 @@ import pl.edu.agh.project_manager.domain.entity.user.Skill; import pl.edu.agh.project_manager.domain.entity.user.User; import pl.edu.agh.project_manager.domain.enums.QualificationStatus; +import pl.edu.agh.project_manager.domain.event.QualificationRequestedEvent; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.repository.user.QualificationRepository; @@ -26,6 +28,7 @@ public class QualificationService { private final QualificationRepository qualificationRepository; private final SkillRepository skillRepository; private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public List addQualificationsToUser(UUID userId, List skillNames, List skillIds) { @@ -67,7 +70,18 @@ private Qualification saveQualification(User user, Skill skill) { .status(QualificationStatus.WAITING) .build(); user.addQualification(qualification); - return qualificationRepository.save(qualification); + Qualification saved = qualificationRepository.save(qualification); + + // FIXME: na ten moment przy jednym zgłoszeniu 5 skilli przychodziłoby 5 powiadomień, raczej tak nie chcemy, ale na razie nie wiem jak będzie wyglądało to na froncie, więc zostawiam zakomentowane +// if (user.getSupervisor() != null) { +// eventPublisher.publishEvent(new QualificationRequestedEvent( +// saved.getId(), +// user.getSupervisor(), +// user.getFullName() + " zgłasza umiejętność: " + skill.getName() +// )); +// } + + return saved; } public List searchSkills(String query) { diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java index 7018586c..32085164 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java @@ -2,11 +2,13 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import pl.edu.agh.project_manager.domain.entity.user.ActivationToken; import pl.edu.agh.project_manager.domain.entity.user.User; import pl.edu.agh.project_manager.domain.enums.UserRole; import pl.edu.agh.project_manager.domain.enums.UserStatus; +import pl.edu.agh.project_manager.domain.event.SystemNewEmployeeEvent; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.infrastructure.email.ConsoleEmailSender; @@ -26,6 +28,7 @@ public class UserInvitationService { private final UserRepository userRepository; private final ActivationTokenRepository tokenRepository; private final EmailSender emailSender; + private final ApplicationEventPublisher eventPublisher; @Transactional public void inviteUser(AdminInviteUserCommand command) { @@ -47,6 +50,14 @@ private void processInvitation(String email, UserRole role, UUID supervisorId) { var newUser = createInvitedUser(email, role, supervisor); userRepository.save(newUser); + if (supervisor != null) { + eventPublisher.publishEvent(new SystemNewEmployeeEvent( + newUser.getId(), + supervisor, + "Do twojego zespołu zaproszono nowego pracownika: " + email + )); + } + var activationToken = createTokenForUser(newUser); sendInvitationEmail(email, activationToken); From 7733169976222a5fa1cd93d59cc2042b46d5e5c9 Mon Sep 17 00:00:00 2001 From: mtrznadel24 Date: Fri, 8 May 2026 11:46:42 +0200 Subject: [PATCH 5/6] test(notification): add NotificationService tests --- .../notification/NotificationServiceTest.java | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java new file mode 100644 index 00000000..10de1ed8 --- /dev/null +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java @@ -0,0 +1,137 @@ +package pl.edu.agh.project_manager.service.notification; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import pl.edu.agh.project_manager.controller.dto.notification.NotificationResponse; +import pl.edu.agh.project_manager.domain.entity.notification.Notification; +import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; +import pl.edu.agh.project_manager.domain.event.AssignmentRequestedEvent; +import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; +import pl.edu.agh.project_manager.domain.exception.ApplicationException; +import pl.edu.agh.project_manager.repository.notification.NotificationRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + + @Mock + private NotificationRepository notificationRepository; + + @InjectMocks + private NotificationService notificationService; + + @Test + void shouldCreateAndSaveNotificationOnAssignmentRequestedEvent() { + // Given + User recipient = new User(); + recipient.setId(UUID.randomUUID()); + UUID assignmentId = UUID.randomUUID(); + String message = "Prośba o przypisanie"; + + AssignmentRequestedEvent event = new AssignmentRequestedEvent(assignmentId, recipient, message); + + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); + + // When + notificationService.onAssignmentRequested(event); + + // Then + verify(notificationRepository).save(notificationCaptor.capture()); + Notification savedNotification = notificationCaptor.getValue(); + + assertEquals(recipient, savedNotification.getRecipient()); + assertEquals(NotificationType.ASSIGNMENT_REQUESTED, savedNotification.getType()); + assertEquals(message, savedNotification.getMessage()); + assertEquals(assignmentId, savedNotification.getReferenceId()); + assertFalse(savedNotification.isRead()); + } + + @Test + void shouldReturnMappedNotificationsForUser() { + // Given + UUID userId = UUID.randomUUID(); + int limit = 10; + Pageable expectedPageable = PageRequest.of(0, limit); + + Notification notification = Notification.builder() + .id(UUID.randomUUID()) + .type(NotificationType.SYSTEM_NEW_EMPLOYEE) + .message("Test") + .isRead(false) + .createdAt(LocalDateTime.now()) + .build(); + + when(notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId, expectedPageable)) + .thenReturn(List.of(notification)); + + // When + List result = notificationService.getNotificationsForUser(userId, limit); + + // Then + assertEquals(1, result.size()); + assertEquals(notification.getId(), result.get(0).id()); + assertEquals(notification.getMessage(), result.get(0).message()); + } + + @Test + void shouldMarkNotificationAsRead() { + // Given + UUID notificationId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + Notification notification = new Notification(); + notification.setId(notificationId); + notification.setRead(false); + + when(notificationRepository.findById(notificationId)).thenReturn(Optional.of(notification)); + + // When + notificationService.markAsRead(notificationId, userId); + + // Then + assertTrue(notification.isRead()); + } + + @Test + void shouldThrowExceptionWhenMarkingNonExistentNotificationAsRead() { + // Given + UUID notificationId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + when(notificationRepository.findById(notificationId)).thenReturn(Optional.empty()); + + // When & Then + ApplicationException exception = assertThrows( + ApplicationException.class, + () -> notificationService.markAsRead(notificationId, userId) + ); + + assertEquals(ApiErrorCode.NOTIFICATION_NOT_FOUND, exception.getErrorCode()); + } + + @Test + void shouldMarkAllAsRead() { + // Given + UUID userId = UUID.randomUUID(); + + // When + notificationService.markAllAsRead(userId); + + // Then + verify(notificationRepository).markAllAsReadByUserId(userId); + } +} \ No newline at end of file From e4d4b4d90531fea2484d0333ce65763bcc9c23e9 Mon Sep 17 00:00:00 2001 From: mtrznadel24 Date: Sun, 10 May 2026 16:29:18 +0200 Subject: [PATCH 6/6] test(notification): Create interface for event records making NotificationService thinner with only one method onNotificationEvent --- .../notification/NotificationController.java | 22 +++++-- .../domain/event/AssignmentAcceptedEvent.java | 14 +++- .../domain/event/AssignmentRejectedEvent.java | 14 +++- .../event/AssignmentRequestedEvent.java | 16 ++++- .../domain/event/NotificationEvent.java | 13 ++++ .../event/QualificationAcceptedEvent.java | 14 +++- .../event/QualificationRejectedEvent.java | 14 +++- .../event/QualificationRequestedEvent.java | 14 +++- .../domain/event/SystemNewEmployeeEvent.java | 14 +++- .../notification/NotificationRepository.java | 5 +- .../notification/NotificationService.java | 59 +++++------------ .../project/ProjectAssignmentService.java | 4 +- .../service/user/UserInvitationService.java | 1 - .../notification/NotificationServiceTest.java | 64 +++++++++++++++---- 14 files changed, 193 insertions(+), 75 deletions(-) create mode 100644 backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/NotificationEvent.java diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java index 2f3e0295..6f32a483 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/notification/NotificationController.java @@ -1,17 +1,17 @@ -package pl.edu.agh.project_manager.controller; +package pl.edu.agh.project_manager.controller.notification; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import pl.edu.agh.project_manager.controller.dto.PagedResponse; import pl.edu.agh.project_manager.controller.dto.notification.NotificationResponse; -import pl.edu.agh.project_manager.domain.entity.notification.Notification; import pl.edu.agh.project_manager.security.UserPrincipal; import pl.edu.agh.project_manager.service.notification.NotificationService; -import java.util.List; import java.util.UUID; @RestController @@ -22,11 +22,19 @@ public class NotificationController { private final NotificationService notificationService; @GetMapping - public ResponseEntity> getUserNotifications( - @RequestParam(defaultValue = "50") int limit, + public ResponseEntity> getUserNotifications( + @RequestParam(defaultValue = "true") boolean unreadOnly, + @PageableDefault(size = 20) Pageable pageable, @AuthenticationPrincipal UserPrincipal userPrincipal ) { - return ResponseEntity.ok(notificationService.getNotificationsForUser(userPrincipal.userId(), limit)); + return ResponseEntity.ok( + notificationService.getNotificationsForUser( + userPrincipal.userId(), + pageable.getPageNumber(), + pageable.getPageSize(), + unreadOnly + ) + ); } @PatchMapping("/{notificationId}/mark-as-read") diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java index 26e61c16..741850df 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentAcceptedEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record AssignmentAcceptedEvent( UUID assignmentId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return assignmentId; + } + + @Override + public NotificationType type() { + return NotificationType.ASSIGNMENT_ACCEPTED; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java index 57d367a4..97eccd2b 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRejectedEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record AssignmentRejectedEvent( UUID assignmentId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return assignmentId; + } + + @Override + public NotificationType type() { + return NotificationType.ASSIGNMENT_REJECTED; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java index 66ffb6e5..0ecfd997 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/AssignmentRequestedEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; -import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record AssignmentRequestedEvent( UUID assignmentId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return assignmentId; + } + + @Override + public NotificationType type() { + return NotificationType.ASSIGNMENT_REQUESTED; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/NotificationEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/NotificationEvent.java new file mode 100644 index 00000000..958ef54d --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/NotificationEvent.java @@ -0,0 +1,13 @@ +package pl.edu.agh.project_manager.domain.event; + +import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; + +import java.util.UUID; + +public interface NotificationEvent { + User recipient(); + String message(); + UUID referenceId(); + NotificationType type(); +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java index ae4e3b22..ddb54a78 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationAcceptedEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record QualificationAcceptedEvent( UUID qualificationId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return qualificationId; + } + + @Override + public NotificationType type() { + return NotificationType.QUALIFICATION_ACCEPTED; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java index 2b5dda34..5b5ee14e 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRejectedEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record QualificationRejectedEvent( UUID qualificationId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return qualificationId; + } + + @Override + public NotificationType type() { + return NotificationType.QUALIFICATION_REJECTED; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java index 370e3a3d..e0530085 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/QualificationRequestedEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record QualificationRequestedEvent( UUID qualificationId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return qualificationId; + } + + @Override + public NotificationType type() { + return NotificationType.QUALIFICATION_REQUESTED; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java index fb7db3c2..3bc8f816 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/domain/event/SystemNewEmployeeEvent.java @@ -1,6 +1,7 @@ package pl.edu.agh.project_manager.domain.event; import pl.edu.agh.project_manager.domain.entity.user.User; +import pl.edu.agh.project_manager.domain.enums.NotificationType; import java.util.UUID; @@ -8,4 +9,15 @@ public record SystemNewEmployeeEvent( UUID employeeId, User recipient, String message -) {} \ No newline at end of file +) implements NotificationEvent { + + @Override + public UUID referenceId() { + return employeeId; + } + + @Override + public NotificationType type() { + return NotificationType.SYSTEM_NEW_EMPLOYEE; + } +} \ No newline at end of file diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java index c201a6d7..0d4282db 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/notification/NotificationRepository.java @@ -1,5 +1,6 @@ package pl.edu.agh.project_manager.repository.notification; +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.Modifying; @@ -14,7 +15,9 @@ public interface NotificationRepository extends JpaRepository findAllByRecipientIdOrderByCreatedAtDesc(UUID recipientId, Pageable pageable); + Page findAllByRecipientIdOrderByCreatedAtDesc(UUID recipientId, Pageable pageable); + + Page findAllByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(UUID recipientId, Pageable pageable); @Modifying @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :userId AND n.isRead = false") diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java index b48401ca..d0385e83 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/notification/NotificationService.java @@ -1,12 +1,14 @@ package pl.edu.agh.project_manager.service.notification; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; +import pl.edu.agh.project_manager.controller.dto.PagedResponse; import pl.edu.agh.project_manager.controller.dto.notification.NotificationResponse; import pl.edu.agh.project_manager.domain.entity.notification.Notification; import pl.edu.agh.project_manager.domain.entity.user.User; @@ -27,44 +29,8 @@ public class NotificationService { @TransactionalEventListener @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onAssignmentRequested(AssignmentRequestedEvent event) { - create(event.recipient(), NotificationType.ASSIGNMENT_REQUESTED, event.message(), event.assignmentId()); - } - - @TransactionalEventListener - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onAssignmentAccepted(AssignmentAcceptedEvent event) { - create(event.recipient(), NotificationType.ASSIGNMENT_ACCEPTED, event.message(), event.assignmentId()); - } - - @TransactionalEventListener - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onAssignmentRejected(AssignmentRejectedEvent event) { - create(event.recipient(), NotificationType.ASSIGNMENT_REJECTED, event.message(), event.assignmentId()); - } - - @TransactionalEventListener - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onQualificationRequested(QualificationRequestedEvent event) { - create(event.recipient(), NotificationType.QUALIFICATION_REQUESTED, event.message(), event.qualificationId()); - } - - @TransactionalEventListener - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onQualificationAccepted(QualificationAcceptedEvent event) { - create(event.recipient(), NotificationType.QUALIFICATION_ACCEPTED, event.message(), event.qualificationId()); - } - - @TransactionalEventListener - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onQualificationRejected(QualificationRejectedEvent event) { - create(event.recipient(), NotificationType.QUALIFICATION_REJECTED, event.message(), event.qualificationId()); - } - - @TransactionalEventListener - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void onSystemNewEmployee(SystemNewEmployeeEvent event) { - create(event.recipient(), NotificationType.SYSTEM_NEW_EMPLOYEE, event.message(), event.employeeId()); + public void onNotificationEvent(NotificationEvent event) { + create(event.recipient(), event.type(), event.message(), event.referenceId()); } private void create(User recipient, NotificationType type, String message, UUID referenceId) { @@ -78,13 +44,18 @@ private void create(User recipient, NotificationType type, String message, UUID } @Transactional(readOnly = true) - public List getNotificationsForUser(UUID userId, int limit) { - Pageable pageable = PageRequest.of(0, limit); + public PagedResponse getNotificationsForUser(UUID userId, int page, int size, boolean unreadOnly) { + Pageable pageable = PageRequest.of(page, size); + + Page notificationPage; + + if (unreadOnly) { + notificationPage = notificationRepository.findAllByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(userId, pageable); + } else { + notificationPage = notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId, pageable); + } - return notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId, pageable) - .stream() - .map(NotificationResponse::from) - .toList(); + return PagedResponse.from(notificationPage, NotificationResponse::from); } @Transactional diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java index 4738f358..18e1e038 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java @@ -54,7 +54,7 @@ public AssignmentResponse createAssignment(UUID projectId, AssignmentCommand com eventPublisher.publishEvent(new AssignmentRequestedEvent( savedAssignment.getId(), user.getSupervisor(), - "Kierownik projektu " + project.getTitle() + " prosi o alokację pracownika " + user.getName() + " " + user.getSurname() + "Kierownik projektu " + project.getTitle() + " prosi o alokację pracownika " + user.getFullName() )); } @@ -125,7 +125,7 @@ public void acceptAssignment(UUID assignmentId) { eventPublisher.publishEvent(new AssignmentAcceptedEvent( assignment.getId(), project.getProjectManager(), - "Zaakceptowano przypisanie pracownika " + user.getName() + " do projektu " + project.getTitle() + "Zaakceptowano przypisanie pracownika " + user.getFullName() + " do projektu " + project.getTitle() )); } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java index 32085164..16ee3f33 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/user/UserInvitationService.java @@ -11,7 +11,6 @@ import pl.edu.agh.project_manager.domain.event.SystemNewEmployeeEvent; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; -import pl.edu.agh.project_manager.infrastructure.email.ConsoleEmailSender; import pl.edu.agh.project_manager.service.command.invitation.AdminInviteUserCommand; import pl.edu.agh.project_manager.repository.user.ActivationTokenRepository; import pl.edu.agh.project_manager.repository.user.UserRepository; diff --git a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java index 10de1ed8..765fd4cf 100644 --- a/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java +++ b/backend/project-manager/src/test/java/pl/edu/agh/project_manager/service/notification/NotificationServiceTest.java @@ -6,8 +6,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +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 pl.edu.agh.project_manager.controller.dto.PagedResponse; import pl.edu.agh.project_manager.controller.dto.notification.NotificationResponse; import pl.edu.agh.project_manager.domain.entity.notification.Notification; import pl.edu.agh.project_manager.domain.entity.user.User; @@ -35,7 +38,7 @@ class NotificationServiceTest { private NotificationService notificationService; @Test - void shouldCreateAndSaveNotificationOnAssignmentRequestedEvent() { + void shouldCreateAndSaveNotificationWhenNotificationEventIsPublished() { // Given User recipient = new User(); recipient.setId(UUID.randomUUID()); @@ -47,7 +50,7 @@ void shouldCreateAndSaveNotificationOnAssignmentRequestedEvent() { ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); // When - notificationService.onAssignmentRequested(event); + notificationService.onNotificationEvent(event); // Then verify(notificationRepository).save(notificationCaptor.capture()); @@ -61,30 +64,67 @@ void shouldCreateAndSaveNotificationOnAssignmentRequestedEvent() { } @Test - void shouldReturnMappedNotificationsForUser() { + void shouldReturnPagedNotificationsForUserWhenUnreadOnlyIsFalse() { // Given UUID userId = UUID.randomUUID(); - int limit = 10; - Pageable expectedPageable = PageRequest.of(0, limit); + int page = 0; + int size = 10; + Pageable expectedPageable = PageRequest.of(page, size); Notification notification = Notification.builder() .id(UUID.randomUUID()) .type(NotificationType.SYSTEM_NEW_EMPLOYEE) - .message("Test") - .isRead(false) + .message("Test All") + .isRead(true) .createdAt(LocalDateTime.now()) .build(); + Page notificationPage = new PageImpl<>(List.of(notification), expectedPageable, 1); + when(notificationRepository.findAllByRecipientIdOrderByCreatedAtDesc(userId, expectedPageable)) - .thenReturn(List.of(notification)); + .thenReturn(notificationPage); + + // When + PagedResponse result = notificationService.getNotificationsForUser(userId, page, size, false); + + // Then + assertEquals(1, result.items().size()); + assertEquals(notification.getId(), result.items().get(0).id()); + assertEquals("Test All", result.items().get(0).message()); + assertEquals(1, result.totalCount()); + assertEquals(0, result.pageNumber()); + } + + @Test + void shouldReturnPagedNotificationsForUserWhenUnreadOnlyIsTrue() { + // Given + UUID userId = UUID.randomUUID(); + int page = 0; + int size = 10; + Pageable expectedPageable = PageRequest.of(page, size); + + Notification notification = Notification.builder() + .id(UUID.randomUUID()) + .type(NotificationType.ASSIGNMENT_REQUESTED) + .message("Test Unread") + .isRead(false) + .createdAt(LocalDateTime.now()) + .build(); + + Page notificationPage = new PageImpl<>(List.of(notification), expectedPageable, 1); + + when(notificationRepository.findAllByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(userId, expectedPageable)) + .thenReturn(notificationPage); // When - List result = notificationService.getNotificationsForUser(userId, limit); + PagedResponse result = notificationService.getNotificationsForUser(userId, page, size, true); // Then - assertEquals(1, result.size()); - assertEquals(notification.getId(), result.get(0).id()); - assertEquals(notification.getMessage(), result.get(0).message()); + assertEquals(1, result.items().size()); + assertEquals(notification.getId(), result.items().get(0).id()); + assertEquals("Test Unread", result.items().get(0).message()); + assertFalse(result.items().get(0).isRead()); + assertEquals(1, result.totalPages()); } @Test