diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java index 277cf725..02c9be4a 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java @@ -6,15 +6,20 @@ import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaRepository; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; +import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; +import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerUpdateRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse.InquiryAnswerDetailResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class InquiryAnswerService { @@ -22,15 +27,13 @@ public class InquiryAnswerService { private final InquiryAnswerAttachmentService answerAttachmentService; private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository; private final InquiryJpaRepository inquiryJpaRepository; + private final NotifyEventPublisher notifier; @Transactional public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) { isAnswerAlreadyRegister(postId); InquiryJpaEntity inquiryEntity = getInquiry(postId); - - if (inquiryAnswerJpaRepository.findByInquiryId(postId).isPresent()) { - throw new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_ALREADY_EXISTS); - } + Long userId = inquiryEntity.getUserId(); InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.save( request.toEntity(postId)); @@ -38,6 +41,8 @@ public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) { answerAttachmentService.createAttachment(request.attachments(), answerEntity); inquiryEntity.updateStatusToComplete(); + sendNotification(userId, postId); + } @Transactional @@ -82,10 +87,16 @@ private InquiryJpaEntity getInquiry(Long postId) { } private void isAnswerAlreadyRegister(Long postId) { - if (inquiryAnswerJpaRepository.existsById(postId)) { + if (inquiryAnswerJpaRepository.existsByInquiryId(postId)) { throw new CustomRuntimeException(ErrorCode.INQUIRY_ANSWER_ALREADY_EXISTS); } } + private void sendNotification(Long userId, Long postId) { + NotificationEvent event = NotificationEvent.create( + NotificationStatus.INQUIRY_ANSWER_SUCCESS, userId, postId); + notifier.notify(event); + } + } diff --git a/src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java b/src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java index 2e7e875b..5fc43920 100644 --- a/src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java +++ b/src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java @@ -1,20 +1,21 @@ package life.mosu.mosuserver.application.notify; -import java.time.LocalDate; import life.mosu.mosuserver.application.notify.dto.ApplicationNotifyRequest; import life.mosu.mosuserver.application.notify.dto.Exam1DayBeforeNotifyRequest; import life.mosu.mosuserver.application.notify.dto.Exam1WeekBeforeNotifyRequest; import life.mosu.mosuserver.application.notify.dto.Exam3DayBeforeNotifyRequest; import life.mosu.mosuserver.application.notify.dto.InquiryAnswerNotifyRequest; +import life.mosu.mosuserver.application.notify.dto.RefundNotifyRequest; import life.mosu.mosuserver.application.notify.dto.SignUpNotifyRequest; import life.mosu.mosuserver.domain.exam.ExamJpaRepository; import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaRepository; -import life.mosu.mosuserver.domain.examapplication.projection.ExamApplicationNotifyProjection; import life.mosu.mosuserver.domain.examapplication.ExamInfoProjection; +import life.mosu.mosuserver.domain.examapplication.projection.ExamApplicationNotifyProjection; import life.mosu.mosuserver.domain.examapplication.projection.ExamInfoWithExamNumberProjection; import life.mosu.mosuserver.domain.inquiry.InquiryJpaEntity; import life.mosu.mosuserver.domain.inquiry.InquiryJpaRepository; import life.mosu.mosuserver.domain.refund.RefundJpaRepository; +import life.mosu.mosuserver.domain.refund.RefundNotifyProjection; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; @@ -61,29 +62,21 @@ private NotificationVariable createInquiryAnswerVariable(Long targetId) { } private NotificationVariable createRefundVariable(Long targetId) { -// RefundNotifyProjection projection = refundJpaRepository.findRefundByApplicationSchoolId( -// targetId) -// .orElseThrow( -// () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND)); -// return new RefundNotifyRequest( -// projection.paymentKey(), projection.examDate(), projection.schoolName(), -// projection.paymentMethod().getName(), projection.reason() -// ); - return null; + RefundNotifyProjection projection = refundJpaRepository.findRefundByExamApplicationId( + targetId) + .orElseThrow( + () -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND)); + return new RefundNotifyRequest( + projection.paymentKey(), projection.examDate(), projection.schoolName(), + projection.refundAmount(), projection.paymentMethod().getName(), projection.reason() + ); } - // TODO : Exception custom 필요 private NotificationVariable createApplicationVariable(Long targetId) { -// ExamApplicationNotifyProjection projection = examApplicationRepository.findExamAndPaymentByExamApplicationId( -// targetId) -// .orElseThrow(() -> new RuntimeException()); - ExamApplicationNotifyProjection projection = new ExamApplicationNotifyProjection( - "paymentKey", - LocalDate.of(2025, 7, 25), - "schoolName", - true, - "고구마" - ); + ExamApplicationNotifyProjection projection = examApplicationRepository.findExamAndPaymentByExamApplicationId( + targetId) + .orElseThrow( + () -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND)); return ApplicationNotifyRequest.from(projection); } diff --git a/src/main/java/life/mosu/mosuserver/application/notify/dto/RefundNotifyRequest.java b/src/main/java/life/mosu/mosuserver/application/notify/dto/RefundNotifyRequest.java index 367f6b91..2c908575 100644 --- a/src/main/java/life/mosu/mosuserver/application/notify/dto/RefundNotifyRequest.java +++ b/src/main/java/life/mosu/mosuserver/application/notify/dto/RefundNotifyRequest.java @@ -12,7 +12,7 @@ public record RefundNotifyRequest( String paymentKey, LocalDate examDate, String schoolName, -// String amount, + Integer refundAmount, String paymentMethod, String reason ) implements NotificationVariable { @@ -30,7 +30,7 @@ public Map toMap() { "paymentKey", paymentKey, "examDate", examDate.toString(), "schoolName", schoolName, - "amount", String.valueOf(1000), + "refundAmount", String.valueOf(refundAmount), "paymentMethod", paymentMethod, "reason", reason ); diff --git a/src/main/java/life/mosu/mosuserver/application/payment/PaymentEventTxService.java b/src/main/java/life/mosu/mosuserver/application/payment/PaymentEventTxService.java index 33398ee8..59c5e269 100644 --- a/src/main/java/life/mosu/mosuserver/application/payment/PaymentEventTxService.java +++ b/src/main/java/life/mosu/mosuserver/application/payment/PaymentEventTxService.java @@ -19,16 +19,18 @@ public class PaymentEventTxService { private final PaymentTxEventFactory eventFactory; @Transactional - public void publishSuccessEvent(List examApplicationIds, String orderId, int amount) { + public void publishSuccessEvent(List examApplicationIds, String orderId, Long userId, + int amount) { TxEvent event = eventFactory.create( - PaymentContext.ofSuccess(examApplicationIds, orderId, amount)); + PaymentContext.ofSuccess(examApplicationIds, orderId, userId, amount)); txEventPublisher.publish(event); } @Transactional - public void publishFailureEvent(List examApplicationIds, String orderId, int amount) { + public void publishFailureEvent(List examApplicationIds, String orderId, Long userId, + int amount) { TxEvent event = eventFactory.create( - PaymentContext.ofFailure(examApplicationIds, orderId, amount)); + PaymentContext.ofFailure(examApplicationIds, orderId, userId, amount)); txEventPublisher.publish(event); } } diff --git a/src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java b/src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java index f38ef30e..63bc7d57 100644 --- a/src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java +++ b/src/main/java/life/mosu/mosuserver/application/payment/PaymentService.java @@ -45,7 +45,7 @@ public PaymentPrepareResponse prepare(PreparePaymentRequest request) { } @Transactional - public void confirm(PaymentRequest request) { + public void confirm(Long userId, PaymentRequest request) { List applications = getValidApplications(request.applicationId()); String orderId = request.orderId(); int lunchAmount = amountCalculator.calculateLunchAmount(applications); @@ -55,7 +55,7 @@ public void confirm(PaymentRequest request) { .toList(); try { - amountCalculator.verifyAmount(applications.size(), lunchAmount, request.amount()); + amountCalculator.verifyAmount(applications.size(), lunchAmount, request.amount()); validateNoDuplicateOrderId(orderId); ConfirmTossPaymentResponse tossResponse = tossProcessor.process(request); @@ -67,12 +67,13 @@ public void confirm(PaymentRequest request) { ); paymentJpaRepository.saveAll(payments); - eventTxService.publishSuccessEvent(examApplicationIds, orderId, request.amount()); + eventTxService.publishSuccessEvent(examApplicationIds, orderId, userId, + request.amount()); } catch (Exception ex) { log.error("결제 승인 실패: {}", ex.getMessage(), ex); - eventTxService.publishFailureEvent(examApplicationIds, orderId, request.amount()); + eventTxService.publishFailureEvent(examApplicationIds, orderId, userId, + request.amount()); throw ex; - } } diff --git a/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentContext.java b/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentContext.java index d0852342..6d2a29b2 100644 --- a/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentContext.java +++ b/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentContext.java @@ -6,6 +6,7 @@ public record PaymentContext( List examSchoolIds, String orderId, + Long userId, Boolean isSuccess, PaymentStatus status, Integer totalAmount @@ -14,10 +15,12 @@ public record PaymentContext( public static PaymentContext ofSuccess( List examSchoolIds, String orderId, + Long userId, Integer totalAmount) { return new PaymentContext( examSchoolIds, orderId, + userId, true, PaymentStatus.DONE, totalAmount @@ -27,10 +30,12 @@ public static PaymentContext ofSuccess( public static PaymentContext ofFailure( List examSchoolIds, String orderId, + Long userId, Integer totalAmount) { return new PaymentContext( examSchoolIds, orderId, + userId, false, PaymentStatus.ABORTED, totalAmount diff --git a/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentTxEventListener.java b/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentTxEventListener.java index ad4beb95..ccfef4b6 100644 --- a/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentTxEventListener.java +++ b/src/main/java/life/mosu/mosuserver/application/payment/tx/PaymentTxEventListener.java @@ -1,6 +1,9 @@ package life.mosu.mosuserver.application.payment.tx; import life.mosu.mosuserver.global.tx.TxFailureHandler; +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; +import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; +import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -16,6 +19,7 @@ public class PaymentTxEventListener { private final TxFailureHandler paymentFailureHandler; + private final NotifyEventPublisher notifier; @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) public void afterRollbackHandler(PaymentTxEvent event) { @@ -25,5 +29,24 @@ public void afterRollbackHandler(PaymentTxEvent event) { log.info("[AFTER_ROLLBACK] 롤백 후 처리 완료: orderId={}", ctx.orderId()); } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void afterCommitHandler(PaymentTxEvent event) { + PaymentContext ctx = event.getContext(); + log.debug("[AFTER_COMMIT] 결제 성공 후 알림톡 발송 시작: orderId={}", ctx.orderId()); + ctx.examSchoolIds().forEach(examSchoolId -> sendNotification(ctx.userId(), examSchoolId)); + } + + /** + * 외부 Notification Vendor 에 맞춰서 신청 성공에 대한 알림톡 전송 + * + * @param userId + * @param examSchoolId + */ + private void sendNotification(Long userId, Long examSchoolId) { + NotificationEvent notificationEvent = NotificationEvent.create( + NotificationStatus.APPLICATION_SUCCESS, userId, examSchoolId); + notifier.notify(notificationEvent); + } } diff --git a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java index 4f396174..54039360 100644 --- a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java +++ b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java @@ -7,6 +7,9 @@ import life.mosu.mosuserver.domain.user.UserRole; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; +import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; +import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; import life.mosu.mosuserver.presentation.profile.dto.EditProfileRequest; import life.mosu.mosuserver.presentation.profile.dto.ProfileDetailResponse; import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest; @@ -21,6 +24,7 @@ public class ProfileService { private final UserJpaRepository userRepository; private final ProfileJpaRepository profileJpaRepository; + private final NotifyEventPublisher notifier; @Transactional public void registerProfile(Long userId, SignUpProfileRequest request) { @@ -34,6 +38,8 @@ public void registerProfile(Long userId, SignUpProfileRequest request) { user.grantUserRole(); syncUserInfoFromProfile(user, request); + + sendNotification(); } @Transactional @@ -65,5 +71,10 @@ private void syncUserInfoFromProfile(UserJpaEntity user, SignUpProfileRequest re request.phoneNumber(), request.birth()); } } + + private void sendNotification() { + NotificationEvent event = NotificationEvent.create(NotificationStatus.SIGN_UP_SUCCESS); + notifier.notify(event); + } } diff --git a/src/main/java/life/mosu/mosuserver/application/refund/RefundEventTxService.java b/src/main/java/life/mosu/mosuserver/application/refund/RefundEventTxService.java index 237bedf8..1864ac3b 100644 --- a/src/main/java/life/mosu/mosuserver/application/refund/RefundEventTxService.java +++ b/src/main/java/life/mosu/mosuserver/application/refund/RefundEventTxService.java @@ -21,20 +21,24 @@ public class RefundEventTxService { @Transactional public void publishSuccessEvent( String transactionKey, - Integer refundAmount + Integer refundAmount, + Long userId, + Long examApplicationId ) { TxEvent event = eventFactory.create( - RefundContext.ofSuccess(transactionKey, refundAmount)); + RefundContext.ofSuccess(transactionKey, refundAmount, userId, examApplicationId)); txEventPublisher.publish(event); } @Transactional public void publishFailureEvent( String transactionKey, - Integer refundAmount + Integer refundAmount, + Long userId, + Long examApplicationId ) { TxEvent event = eventFactory.create( - RefundContext.ofFailure(transactionKey, refundAmount)); + RefundContext.ofFailure(transactionKey, refundAmount, userId, examApplicationId)); txEventPublisher.publish(event); } } diff --git a/src/main/java/life/mosu/mosuserver/application/refund/RefundService.java b/src/main/java/life/mosu/mosuserver/application/refund/RefundService.java index f06eaf05..a1146b67 100644 --- a/src/main/java/life/mosu/mosuserver/application/refund/RefundService.java +++ b/src/main/java/life/mosu/mosuserver/application/refund/RefundService.java @@ -12,9 +12,9 @@ import life.mosu.mosuserver.presentation.refund.dto.MergedRefundRequest; import life.mosu.mosuserver.presentation.refund.dto.RefundRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import lombok.extern.slf4j.Slf4j; @Slf4j @@ -29,27 +29,32 @@ public class RefundService { private final PaymentJpaRepository paymentJpaRepository; @Transactional - public void doProcess(MergedRefundRequest request) { + public void doProcess(Long userId, MergedRefundRequest request) { RefundRequest details = request.details(); String paymentKey = request.paymentKey(); Long examApplicationId = details.examApplicationId(); - PaymentWithLunchProjection targetPayment = findPaymentOrThrow(paymentKey, examApplicationId); + PaymentWithLunchProjection targetPayment = findPaymentOrThrow(paymentKey, + examApplicationId); int totalQuantity = getTotalPaymentCount(paymentKey); int refundAmount = calculateRefundAmount(totalQuantity, targetPayment.lunchChecked()); - RefundJpaEntity refundEntity = processRefund(request, refundAmount, targetPayment.examApplicationId()); + RefundJpaEntity refundEntity = processRefund(request, refundAmount, + targetPayment.examApplicationId()); try { refundJpaRepository.save(refundEntity); - eventTxService.publishSuccessEvent(refundEntity.getTransactionKey(), refundAmount); + eventTxService.publishSuccessEvent(refundEntity.getTransactionKey(), refundAmount, + userId, examApplicationId); } catch (Exception e) { log.error("환불 이벤트 처리 중 실패", e); - eventTxService.publishFailureEvent(refundEntity.getTransactionKey(), refundAmount); + eventTxService.publishFailureEvent(refundEntity.getTransactionKey(), refundAmount, + userId, examApplicationId); throw e; } } - private PaymentWithLunchProjection findPaymentOrThrow(String paymentKey, Long examApplicationId) { + private PaymentWithLunchProjection findPaymentOrThrow(String paymentKey, + Long examApplicationId) { return paymentJpaRepository.findByPaymentKeyWithLunch(paymentKey).stream() .filter(p -> p.examApplicationId().equals(examApplicationId)) .findFirst() @@ -68,9 +73,11 @@ private int calculateRefundAmount(int totalQuantity, boolean lunchChecked) { } } - private RefundJpaEntity processRefund(MergedRefundRequest request, int refundAmount, Long examApplicationId) { + private RefundJpaEntity processRefund(MergedRefundRequest request, int refundAmount, + Long examApplicationId) { try { - RefundProcessorRequest processorRequest = RefundProcessorRequest.of(request, refundAmount); + RefundProcessorRequest processorRequest = RefundProcessorRequest.of(request, + refundAmount); CancelTossPaymentResponse response = tossRefundProcessor.process(processorRequest); return response.toEntity(examApplicationId); } catch (Exception e) { diff --git a/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundContext.java b/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundContext.java index b3a63961..334310d0 100644 --- a/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundContext.java +++ b/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundContext.java @@ -5,28 +5,38 @@ public record RefundContext( String transactionKey, Integer refundAmount, + Long userId, + Long examApplicationId, Boolean isSuccess, RefundStatus status ) { public static RefundContext ofSuccess( String transactionKey, - Integer refundAmount + Integer refundAmount, + Long userId, + Long examApplicationId ) { return new RefundContext( transactionKey, refundAmount, + userId, + examApplicationId, true, RefundStatus.DONE); } public static RefundContext ofFailure( String transactionKey, - Integer refundAmount + Integer refundAmount, + Long userId, + Long examApplicationId ) { return new RefundContext( transactionKey, refundAmount, + userId, + examApplicationId, false, RefundStatus.ABORTED ); diff --git a/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundTxEventListener.java b/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundTxEventListener.java index c535bb2e..3dc62555 100644 --- a/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundTxEventListener.java +++ b/src/main/java/life/mosu/mosuserver/application/refund/tx/RefundTxEventListener.java @@ -1,6 +1,9 @@ package life.mosu.mosuserver.application.refund.tx; import life.mosu.mosuserver.global.tx.TxFailureHandler; +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; +import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; +import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -13,6 +16,7 @@ public class RefundTxEventListener { private final TxFailureHandler refundFailureHandler; + private final NotifyEventPublisher notifier; @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) public void afterRollbackHandler(RefundTxEvent event) { @@ -22,4 +26,17 @@ public void afterRollbackHandler(RefundTxEvent event) { log.info("[AFTER_ROLLBACK] 롤백 후 처리 완료: orderId={}", ctx.transactionKey()); } + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void afterCommitHandler(RefundTxEvent event) { + RefundContext ctx = event.getContext(); + log.info("[AFTER_COMMIT] 환불 성공 후 알림톡 발송 시작: orderId={}", ctx.transactionKey()); + sendNotification(ctx.userId(), ctx.examApplicationId()); + } + + private void sendNotification(Long userId, Long examApplicationId) { + NotificationEvent notificationEvent = NotificationEvent.create( + NotificationStatus.REFUND_SUCCESS, + userId, examApplicationId); + notifier.notify(notificationEvent); + } } diff --git a/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java index bf683038..2992f655 100644 --- a/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/examapplication/ExamApplicationJpaRepository.java @@ -88,7 +88,8 @@ Optional findExamTicketInfoProjectionById( WHERE ea.id = :targetId AND p.paymentStatus = 'DONE' """) - Optional findExamAndPaymentByExamApplicationId(Long targetId); + Optional findExamAndPaymentByExamApplicationId( + @Param("targetId") Long targetId); @Query(""" diff --git a/src/main/java/life/mosu/mosuserver/domain/inquiryAnswer/InquiryAnswerJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/inquiryAnswer/InquiryAnswerJpaRepository.java index 91a75bf5..d6ee64fd 100644 --- a/src/main/java/life/mosu/mosuserver/domain/inquiryAnswer/InquiryAnswerJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/inquiryAnswer/InquiryAnswerJpaRepository.java @@ -6,4 +6,6 @@ public interface InquiryAnswerJpaRepository extends JpaRepository { Optional findByInquiryId(Long id); + + boolean existsByInquiryId(Long inquiryId); } diff --git a/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java index 2d614735..13af884b 100644 --- a/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/refund/RefundJpaRepository.java @@ -1,24 +1,35 @@ package life.mosu.mosuserver.domain.refund; +import io.lettuce.core.dynamic.annotation.Param; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface RefundJpaRepository extends JpaRepository { -// @Query(""" -// SELECT new life.mosu.mosuserver.domain.refund.RefundNotifyProjection( -// p.paymentKey, -// a.examDate, -// a.schoolName, -// p.paymentMethod, -// r.reason -// ) -// FROM RefundJpaEntity r -// LEFT JOIN PaymentJpaEntity p ON r.applicationSchoolId = p.applicationSchoolId -// LEFT JOIN ApplicationSchoolJpaEntity a ON r.applicationSchoolId = a.id -// WHERE r.applicationSchoolId = :applicationSchoolId -// """) -// Optional findRefundByApplicationSchoolId(Long applicationSchoolId); - Optional findByTransactionKey(String transactionKey); + /** + * String paymentKey, LocalDate examDate, String schoolName, Integer refundAmount, PaymentMethod + * paymentMethod, String reason + */ + + @Query(""" + SELECT new life.mosu.mosuserver.domain.refund.RefundNotifyProjection( + p.paymentKey, + e.examDate, + e.schoolName, + r.refundedAmount, + p.paymentMethod, + r.reason + ) + FROM RefundJpaEntity r + LEFT JOIN PaymentJpaEntity p ON r.examApplicationId = p.examApplicationId + LEFT JOIN ExamApplicationJpaEntity ea ON r.examApplicationId = ea.id + LEFT JOIN ExamJpaEntity e ON ea.examId = e.id + WHERE r.examApplicationId = :examApplicationId + AND r.refundStatus = 'DONE' + """) + Optional findRefundByExamApplicationId( + @Param("examApplicationId") Long examApplicationId); + Optional findByTransactionKey(String transactionKey); } diff --git a/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java b/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java index c7aa3ace..f1237c66 100644 --- a/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java +++ b/src/main/java/life/mosu/mosuserver/domain/refund/RefundNotifyProjection.java @@ -7,9 +7,9 @@ public record RefundNotifyProjection( String paymentKey, LocalDate examDate, String schoolName, + Integer refundAmount, PaymentMethod paymentMethod, String reason - // TODO: 환불 금액 추가 ) { } diff --git a/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java b/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java index a1da14fc..48265376 100644 --- a/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java +++ b/src/main/java/life/mosu/mosuserver/global/initializer/DatabaseInitializer.java @@ -106,7 +106,7 @@ private List createUsersAndProfiles(Random random) { .password(passwordEncoder.encode("Password!" + i)) .gender(i % 2 == 0 ? Gender.MALE : Gender.FEMALE) .name("모수학생" + i) - .phoneNumber("01097348825") + .phoneNumber("010-9161-2960") .birth(LocalDate.of(2005 + i % 3, (i % 12) + 1, (i % 28) + 1)) .userRole(i == 1 ? UserRole.ROLE_ADMIN : UserRole.ROLE_USER) .agreedToTermsOfService(true) @@ -131,9 +131,43 @@ private List createUsersAndProfiles(Random random) { profileRepository.save(profile); } + createAdditionalUsers(); + return users; } + private void createAdditionalUsers() { + for (int i = 11; i <= 12; i++) { + UserJpaEntity user = UserJpaEntity.builder() + .loginId("userid" + i) + .password(passwordEncoder.encode("Password!" + i)) + .gender(i % 2 == 0 ? Gender.MALE : Gender.FEMALE) + .name("모수학생" + i) + .phoneNumber("010-9161-2960") + .birth(LocalDate.of(2005 + i % 3, (i % 12) + 1, (i % 28) + 1)) + .userRole(UserRole.ROLE_USER) + .agreedToTermsOfService(true) + .agreedToPrivacyPolicy(true) + .agreedToMarketing(true) + .provider(AuthProvider.MOSU) + .build(); + userRepository.save(user); + + ProfileJpaEntity profile = ProfileJpaEntity.builder() + .userId(user.getId()) + .userName(user.getName()) + .gender(user.getGender()) + .birth(user.getBirth()) + .phoneNumber(user.getPhoneNumber()) + .email("user" + i + "@mosu.life") + .education(Education.ENROLLED) + .schoolInfo(new SchoolInfoJpaVO("모수고등학교", "서울시 모수구 123", "12345")) + .grade(Grade.HIGH_1) + .build(); + profileRepository.save(profile); + } + } + private List createExams() { List exams = List.of( createExam("대치중학교", Area.DAECHI, "06234", "강남구 대치동 987", LocalDate.of(2025, 10, 19), diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java index acbbcd29..13a6b789 100644 --- a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java @@ -6,8 +6,15 @@ public record NotificationEvent( Long targetId ) { - public static NotificationEvent create(NotificationStatus status, Long userId, Long targetId) { - return new NotificationEvent(status, userId, targetId); + public static NotificationEvent create(NotificationStatus status) { + if (!NotificationStatus.SIGN_UP_SUCCESS.equals(status)) { + throw new IllegalArgumentException("Unknown notification status: " + status); + } + return new NotificationEvent(status, null, null); } + public static NotificationEvent create(NotificationStatus status, Long userId, + Long targetId) { + return new NotificationEvent(status, userId, targetId); + } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/presentation/payment/PaymentWidgetController.java b/src/main/java/life/mosu/mosuserver/presentation/payment/PaymentWidgetController.java index 886f3c87..59ed0d72 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/payment/PaymentWidgetController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/payment/PaymentWidgetController.java @@ -4,14 +4,12 @@ import life.mosu.mosuserver.application.payment.PaymentService; import life.mosu.mosuserver.global.annotation.UserId; import life.mosu.mosuserver.global.util.ApiResponseWrapper; -import life.mosu.mosuserver.presentation.payment.dto.CancelPaymentRequest; import life.mosu.mosuserver.presentation.payment.dto.PaymentPrepareResponse; import life.mosu.mosuserver.presentation.payment.dto.PaymentRequest; import life.mosu.mosuserver.presentation.payment.dto.PreparePaymentRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -47,7 +45,7 @@ public ApiResponseWrapper confirm( @UserId Long userId, @RequestBody PaymentRequest request ) { - paymentService.confirm(request); + paymentService.confirm(userId, request); return ApiResponseWrapper.success(HttpStatus.CREATED, "결제 승인 성공"); } } diff --git a/src/main/java/life/mosu/mosuserver/presentation/refund/RefundController.java b/src/main/java/life/mosu/mosuserver/presentation/refund/RefundController.java index 0163fbd7..c29e1ce2 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/refund/RefundController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/refund/RefundController.java @@ -1,14 +1,15 @@ package life.mosu.mosuserver.presentation.refund; import life.mosu.mosuserver.application.refund.RefundService; +import life.mosu.mosuserver.global.annotation.UserId; import life.mosu.mosuserver.global.util.ApiResponseWrapper; -import life.mosu.mosuserver.presentation.refund.annotation.MergedRefund; import life.mosu.mosuserver.presentation.refund.dto.MergedRefundRequest; import life.mosu.mosuserver.presentation.refund.dto.RefundRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -24,11 +25,13 @@ public class RefundController { private final RefundService refundService; @PostMapping("/{paymentKey}") + @PreAuthorize("isAuthenticated() and hasRole('USER')") ResponseEntity> process( + @UserId Long userId, @PathVariable String paymentKey, // Path Variable @RequestBody RefundRequest refundRequest) { MergedRefundRequest request = MergedRefundRequest.of(paymentKey, refundRequest); - refundService.doProcess(request); + refundService.doProcess(userId, request); return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "결제 취소 성공")); } } diff --git a/src/main/resources/messages_ko.properties b/src/main/resources/messages_ko.properties index 2943c9e7..84f4aca7 100644 --- a/src/main/resources/messages_ko.properties +++ b/src/main/resources/messages_ko.properties @@ -5,7 +5,7 @@ notify.exam.application.complete.alimtalk=\ \u25A0 \uC2DC\uD5D8\uC7A5\uC18C: #{schoolName}\n\ \u25A0 \uB3C4\uC2DC\uB77D: #{lunch}\n\n\ \uC2E0\uCCAD\uB0B4\uC5ED\uC740 \uB9C8\uC774\uD398\uC774\uC9C0\uC5D0\uC11C \uD655\uC778 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n\n\ -\uC2DC\uD5D8 1\uC8FC\uC77C \uC804, \uC2DC\uD5D8 \uAD00\uB828 \uC720\uC758\uC0AC\uD56D\uACFC \uC218\uD5D8\uD45C \uC548\uB0B4 \uB9AC\uB9C8\uC778\uB4DC \uC54C\uB9BC\uC774 \uBC1C\uC1A1\uB420 \uC608\uC815\uC785\uB2C8\uB2E4. +\uC2DC\uD5D8 1\uC8FC\uC77C \uC804,\uC2DC\uD5D8 \uAD00\uB828 \uC720\uC758\uC0AC\uD56D\uACFC \uC218\uD5D8\uD45C \uC548\uB0B4 \uB9AC\uB9C8\uC778\uB4DC \uC54C\uB9BC\uC774 \uBC1C\uC1A1\uB420 \uC608\uC815\uC785\uB2C8\uB2E4. notify.exam.application.complete.sms=\ [\uBAA8\uC218] \uBAA8\uC758\uC218\uB2A5 \uC2E0\uCCAD\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!\n\n\ \u25A0 \uACB0\uC81C\uBC88\uD638: #{paymentKey}\n\ @@ -14,7 +14,7 @@ notify.exam.application.complete.sms=\ \u25A0 \uB3C4\uC2DC\uB77D: #{lunch}\n\n\ \uC2E0\uCCAD\uB0B4\uC5ED\uC740 \uB9C8\uC774\uD398\uC774\uC9C0\uC5D0\uC11C \uD655\uC778 \uAC00\uB2A5\uD569\uB2C8\uB2E4.\n\ \uB9C8\uC774\uD398\uC774\uC9C0 \uBC14\uB85C\uAC00\uAE30: https://www.mosuedu.com/mypage\n\n\ -\uC2DC\uD5D8 1\uC8FC\uC77C \uC804, \uC2DC\uD5D8 \uAD00\uB828 \uC720\uC758\uC0AC\uD56D\uACFC \uC218\uD5D8\uD45C \uC548\uB0B4 \uB9AC\uB9C8\uC778\uB4DC \uC54C\uB9BC\uC774 \uBC1C\uC1A1\uB420 \uC608\uC815\uC785\uB2C8\uB2E4.\n\ +\uC2DC\uD5D8 1\uC8FC\uC77C \uC804,\uC2DC\uD5D8 \uAD00\uB828 \uC720\uC758\uC0AC\uD56D\uACFC \uC218\uD5D8\uD45C \uC548\uB0B4 \uB9AC\uB9C8\uC778\uB4DC \uC54C\uB9BC\uC774 \uBC1C\uC1A1\uB420 \uC608\uC815\uC785\uB2C8\uB2E4.\n\ \uC720\uC758\uC0AC\uD56D \uBC14\uB85C\uAC00\uAE30: https://www.mosuedu.com/warning notify.exam.oneweek.reminder.alimtalk=\ [\uBAA8\uC218] \uBAA8\uC758\uC218\uB2A5\uC774 1\uC8FC\uC77C \uC55E\uC73C\uB85C \uB2E4\uAC00\uC654\uC2B5\uB2C8\uB2E4!\n\n\ @@ -118,7 +118,7 @@ notify.signup.complete.alimtalk=\ \uC9C0\uAE08 \uB2F9\uC7A5 \uBAA8\uC218\uC640 \uD568\uAED8 \uC218\uB2A5\uC744 \uBBF8\uB9AC \uACBD\uD5D8\uD574 \uBCF4\uC138\uC694! notify.inquiry.answered.alimtalk=\ [\uBAA8\uC218] \uBB38\uC758\uD558\uC2E0 \uB0B4\uC6A9\uC5D0 \uB2F5\uBCC0\uC774 \uB4F1\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n\n\ -\uC81C\uBAA9: #{inquiryTitle}\n\n\ +\u25A0 \uC81C\uBAA9: #{inquiryTitle}\n\n\ \uB2F5\uBCC0\uC740 [\uBB38\uC758\uD558\uAE30 > \uB0B4 \uBB38\uC758\uAE00 \uC870\uD68C]\uC5D0\uC11C \uD655\uC778\uD558\uC2E4 \uC218 \uC788\uC2B5\uB2C8\uB2E4. notify.refund.complete.alimtalk=\ [\uBAA8\uC218] \uD658\uBD88\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.\n\n\