NotificationPublisher.java
package com.hwhub.backend.application.service.notification;
import com.hwhub.backend.domain.enums.NotificationLinkType;
import com.hwhub.backend.domain.enums.NotificationType;
import com.hwhub.backend.domain.model.HouseholdMemberModel;
import com.hwhub.backend.domain.model.HouseholdModel;
import com.hwhub.backend.domain.model.HouseworkTaskModel;
import com.hwhub.backend.domain.model.UserModel;
import com.hwhub.backend.domain.model.notification.NotificationEventModel;
import com.hwhub.backend.domain.model.notification.NotificationLink;
import com.hwhub.backend.domain.model.notification.NotificationMessage;
import com.hwhub.backend.domain.model.notification.NotificationModel;
import com.hwhub.backend.domain.repository.HouseholdMemberRepository;
import com.hwhub.backend.domain.repository.HouseholdRepository;
import com.hwhub.backend.domain.repository.NotificationEventRepository;
import com.hwhub.backend.domain.repository.NotificationRepository;
import com.hwhub.backend.domain.repository.UserRepository;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class NotificationPublisher {
private final NotificationPermissionService permissionService;
private final NotificationRepository notificationRepository;
private final NotificationEventRepository notificationEventRepository;
private final HouseholdRepository householdRepository;
private final HouseholdMemberRepository householdMemberRepository;
private final UserRepository userRepository;
/**
* 世帯から削除された場合の通知。
*
* <ul>
* <li>同期:通知センターに即表示
* <li>通知先:削除された対象者
* <li>遷移先:なし
* </ul>
*
* @param householdId 世帯ID
* @param operatorUserId 操作者のユーザーID(世帯のオーナー)
* @param targetUserId 通知対象のユーザーID(削除されたメンバー)
* @param program プログラム名
*/
@Transactional
public void publishRemoveMemberByOwner(
Long householdId, Long operatorUserId, Long targetUserId, String program) {
if (!permissionService.canReceive(targetUserId, NotificationType.HAVE_BEEN_REMOVED)) {
return;
}
// i18n key + params
HouseholdModel household = householdRepository.findById(householdId);
Map<String, Object> params = new HashMap<>();
params.put("householdName", household.getName());
NotificationMessage message =
new NotificationMessage(
"notifications.messages.removedFromHousehold.title",
"notifications.messages.removedFromHousehold.body",
params);
// 通知発生日時
LocalDateTime now = LocalDateTime.now();
NotificationModel model =
NotificationModel.newUnread(
householdId,
NotificationType.HAVE_BEEN_REMOVED,
operatorUserId,
targetUserId,
message,
NotificationLink.none(),
now);
notificationRepository.insert(model, program);
}
/**
* 世帯から自発的に離脱した場合の通知。
*
* <ul>
* <li>同期:通知センターに即表示
* <li>通知先:世帯のメンバー全員
* <li>遷移先:おうち設定
* </ul>
*
* @param householdId 世帯ID
* @param removedUserId 離脱したメンバーのユーザーID
* @param removedUserName 離脱したメンバーの表示名
* @param program プログラム名
*/
@Transactional
public void publishMemberRemoved(
Long householdId, Long removedUserId, String removedUserName, String program) {
// 世帯メンバーの取得
List<HouseholdMemberModel> members =
householdMemberRepository.findActiveByHouseholdId(householdId);
List<HouseholdMemberModel> allowedMembers =
members.stream()
.filter(
member ->
permissionService.canReceive(
member.getUserId(), NotificationType.LEFT_THE_HOUSEHOLD))
.toList();
if (allowedMembers.isEmpty()) {
return;
}
// i18n key + params
HouseholdModel household = householdRepository.findById(householdId);
if (removedUserName == null) {
Optional<UserModel> userOpt = userRepository.findById(removedUserId);
if (!userOpt.isEmpty()) {
removedUserName = userOpt.get().getDisplayName();
}
}
Map<String, Object> params = new HashMap<>();
params.put("householdName", household.getName());
params.put("memberName", removedUserName);
NotificationMessage message =
new NotificationMessage(
"notifications.messages.leftHousehold.title",
"notifications.messages.leftHousehold.body",
params);
NotificationLink link = new NotificationLink(NotificationLinkType.HOUSEHOLD, householdId);
// 通知発生日時
LocalDateTime now = LocalDateTime.now();
List<NotificationModel> notifications =
allowedMembers.stream()
.map(
member ->
NotificationModel.newUnread(
householdId,
NotificationType.LEFT_THE_HOUSEHOLD,
removedUserId,
member.getUserId(),
message,
link,
now))
.toList();
notificationRepository.bulkInsert(notifications, program);
}
/**
* 招待を承認した場合の通知。
*
* <ul>
* <li>同期:通知センターに即表示
* <li>通知先:招待の作成者
* <li>遷移先:おうち設定
* </ul>
*
* @param householdId 世帯ID
* @param acceptedUserId 承認したメンバーのユーザーID
* @param inviterUserId 招待の作成者
* @param program プログラム名
*/
@Transactional
public void publishAcceptInvitation(
Long householdId, Long acceptedUserId, Long inviterUserId, String program) {
if (!permissionService.canReceive(inviterUserId, NotificationType.INVITATION_ACCEPTED)) {
return;
}
// i18n key + params
HouseholdModel household = householdRepository.findById(householdId);
Map<String, Object> params = new HashMap<>();
params.put("householdName", household.getName());
Optional<UserModel> userOpt = userRepository.findById(acceptedUserId);
if (!userOpt.isEmpty()) {
params.put("memberName", userOpt.get().getDisplayName());
}
NotificationMessage message =
new NotificationMessage(
"notifications.messages.acceptInvitation.title",
"notifications.messages.acceptInvitation.body",
params);
NotificationLink link = new NotificationLink(NotificationLinkType.INVITATION, householdId);
LocalDateTime now = LocalDateTime.now();
NotificationModel model =
NotificationModel.newUnread(
householdId,
NotificationType.INVITATION_ACCEPTED,
acceptedUserId,
inviterUserId,
message,
link,
now);
notificationRepository.insert(model, program);
}
/**
* 招待を辞退した場合の通知。
*
* <ul>
* <li>同期:通知センターに即表示
* <li>通知先:招待の作成者
* <li>遷移先:おうち設定
* </ul>
*
* @param householdId 世帯ID
* @param declinedUserId 辞退したメンバーのユーザーID
* @param inviterUserId 招待の作成者
* @param program プログラム名
*/
@Transactional
public void publishDeclineInvitation(
Long householdId, Long declinedUserId, Long inviterUserId, String program) {
if (!permissionService.canReceive(inviterUserId, NotificationType.INVITATION_DECLINED)) {
return;
}
// i18n key + params
HouseholdModel household = householdRepository.findById(householdId);
Map<String, Object> params = new HashMap<>();
params.put("householdName", household.getName());
Optional<UserModel> userOpt = userRepository.findById(declinedUserId);
if (!userOpt.isEmpty()) {
params.put("memberName", userOpt.get().getDisplayName());
}
NotificationMessage message =
new NotificationMessage(
"notifications.messages.declineInvitation.title",
"notifications.messages.declineInvitation.body",
params);
NotificationLink link = new NotificationLink(NotificationLinkType.INVITATION, householdId);
LocalDateTime now = LocalDateTime.now();
NotificationModel model =
NotificationModel.newUnread(
householdId,
NotificationType.INVITATION_DECLINED,
declinedUserId,
inviterUserId,
message,
link,
now);
notificationRepository.insert(model, program);
}
/**
* おうちのオーナーに設定された場合の通知。
*
* <ul>
* <li>同期:通知センターに即表示
* <li>通知先:オーナーになったユーザー
* <li>遷移先:おうち設定
* </ul>
*
* @param householdId 世帯ID
* @param formerOwnerUserId 旧オーナーのユーザーID
* @param newOwnerUserId 新オーナーのユーザーID
* @param program プログラム名
*/
@Transactional
public void publishAssigned2Owner(
Long householdId, Long formerOwnerUserId, Long newOwnerUserId, String program) {
if (!permissionService.canReceive(newOwnerUserId, NotificationType.ASSIGNED_TO_THE_OWNER)) {
return;
}
// i18n key + params
HouseholdModel household = householdRepository.findById(householdId);
Map<String, Object> params = new HashMap<>();
params.put("householdName", household.getName());
NotificationMessage message =
new NotificationMessage(
"notifications.messages.assigned2Owner.title",
"notifications.messages.assigned2Owner.body",
params);
NotificationLink link = new NotificationLink(NotificationLinkType.HOUSEHOLD, householdId);
LocalDateTime now = LocalDateTime.now();
NotificationModel model =
NotificationModel.newUnread(
householdId,
NotificationType.ASSIGNED_TO_THE_OWNER,
formerOwnerUserId,
newOwnerUserId,
message,
link,
now);
notificationRepository.insert(model, program);
}
/**
* タスク割り当てイベント
*
* <ul>
* <li>非同期:eventを一定時間で集約して表示する。集約処理はバッチで行う
* <li>遷移先:MyTasks
* </ul>
*
* 通知先:
*
* <table>
* <tr>
* <td>Before Assignee</td>
* <td>Operator</td>
* <td>After Assignee</td>
* <td>Notification Recipient</td>
* <td>Notification Type</td>
* </tr>
* <tr>
* <td>null</td>
* <td>user1</td>
* <td>user1</td>
* <td>-</td>
* <td>-</td>
* </tr>
* <tr>
* <td>null</td>
* <td>user1</td>
* <td>user2</td>
* <td>user2</td>
* <td>TASK_ASSIGNED</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user1</td>
* <td>null</td>
* <td>-</td>
* <td>-</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user1</td>
* <td>user1</td>
* <td>-</td>
* <td>-</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user1</td>
* <td>user2</td>
* <td>user2</td>
* <td>BE_DUMPED_TASK</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user2</td>
* <td>null</td>
* <td>-</td>
* <td>-</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user2</td>
* <td>user1</td>
* <td>-</td>
* <td>-</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user2</td>
* <td>user2</td>
* <td>user1</td>
* <td>YOUR_TASK_WAS_TAKEN</td>
* </tr>
* <tr>
* <td>user1</td>
* <td>user2</td>
* <td>user3</td>
* <td>user3</td>
* <td>TASK_ASSIGNED</td>
* </tr>
* </table>
*
* @param householdId 世帯ID
* @param actorUserId 操作者のユーザーID
* @param targetUserId 通知対象のユーザーID
* @param taskId タスクID
*/
@Transactional
public void publishTaskAssignedEvent(
HouseworkTaskModel task, Long beforeAssigneeUserId, Long operatorUserId, String program) {
Long afterAssigneeUserId = task.getAssigneeUserId();
// 未割り当て
if (afterAssigneeUserId == null) return;
// 変更なし
if (afterAssigneeUserId.equals(beforeAssigneeUserId)) return;
// 操作者がタスクの担当者
if (operatorUserId != null && operatorUserId.equals(afterAssigneeUserId)) {
// 未割当を自分が取った
if (beforeAssigneeUserId == null) return;
// 他の人からタスクを奪った:beforeAssigneeに通知
NotificationType type = NotificationType.YOUR_TASK_WAS_TAKEN;
Long targetUserId = beforeAssigneeUserId;
insertTaskNotificationEvent(task, type, targetUserId, operatorUserId, program);
} else {
// 操作者が自分以外に割り当てた:afterassigneeに通知
NotificationType type;
Long targetUserId = afterAssigneeUserId;
if (beforeAssigneeUserId == null) {
// 未割当を割り当てた
type = NotificationType.TASK_ASSIGNED;
} else if (operatorUserId != null && operatorUserId.equals(beforeAssigneeUserId)) {
// 他の人にタスクを押し付けた
type = NotificationType.BE_DUMPED_TASK;
} else {
// 第3者がタスクを付け替えた
type = NotificationType.TASK_ASSIGNED;
}
insertTaskNotificationEvent(task, type, targetUserId, operatorUserId, program);
}
}
/**
* 問い合わせに返信があった場合の通知。
*
* <ul>
* <li>同期:通知センターに即表示
* <li>通知先:問い合わせの作成者
* <li>遷移先:問い合わせ詳細
* </ul>
*
* @param inquiryId 問い合わせID
* @param inquiryTitle 問い合わせ件名
* @param targetUserId 通知対象のユーザーID(問い合わせの作成者)
* @param operatorUserId 操作者のユーザーID
* @param program プログラム名
*/
@Transactional
public void publishInquiryReplied(
Long inquiryId, String inquiryTitle, Long targetUserId, Long operatorUserId, String program) {
if (!permissionService.canReceive(
targetUserId, NotificationType.YOUR_INQUIRY_HAS_BEEN_REPLIED)) {
return;
}
Map<String, Object> params = new HashMap<>();
params.put("inquiryId", inquiryId);
params.put("title", inquiryTitle);
NotificationMessage message =
new NotificationMessage(
"notifications.messages.inquiryReplied.title",
"notifications.messages.inquiryReplied.body",
params);
NotificationLink link = new NotificationLink(NotificationLinkType.INQUIRY_DETAIL, inquiryId);
LocalDateTime now = LocalDateTime.now();
NotificationModel model =
NotificationModel.newUnread(
null,
NotificationType.YOUR_INQUIRY_HAS_BEEN_REPLIED,
operatorUserId,
targetUserId,
message,
link,
now);
notificationRepository.insert(model, program);
}
private void insertTaskNotificationEvent(
HouseworkTaskModel task,
NotificationType type,
Long targetUserId,
Long operatorUserId,
String program) {
if (!permissionService.canReceive(targetUserId, type)) {
return;
}
NotificationEventModel event =
NotificationEventModel.newUnprocessed(
task.getHouseholdId(),
type,
operatorUserId,
targetUserId,
task.getHouseworkTaskId(),
task.getTargetDate(),
LocalDateTime.now());
notificationEventRepository.insert(event, operatorUserId, program);
}
}