Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,43 @@
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 {

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));

answerAttachmentService.createAttachment(request.attachments(), answerEntity);
inquiryEntity.updateStatusToComplete();

sendNotification(userId, postId);

}

@Transactional
Expand Down Expand Up @@ -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);
}
Comment on lines +95 to +99
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding error handling for notification failures.

Similar to the refund notification, consider adding error handling to prevent inquiry answer creation from failing due to notification issues.

 private void sendNotification(Long userId, Long postId) {
+    try {
         NotificationEvent event = NotificationEvent.create(
                 NotificationStatus.INQUIRY_ANSWER_SUCCESS, userId, postId);
         notifier.notify(event);
+    } catch (Exception e) {
+        log.error("문의 답변 완료 알림톡 발송 실패: userId={}, postId={}", 
+                userId, postId, e);
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void sendNotification(Long userId, Long postId) {
NotificationEvent event = NotificationEvent.create(
NotificationStatus.INQUIRY_ANSWER_SUCCESS, userId, postId);
notifier.notify(event);
}
private void sendNotification(Long userId, Long postId) {
try {
NotificationEvent event = NotificationEvent.create(
NotificationStatus.INQUIRY_ANSWER_SUCCESS, userId, postId);
notifier.notify(event);
} catch (Exception e) {
log.error("문의 답변 완료 알림톡 발송 실패: userId={}, postId={}",
userId, postId, e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java
around lines 95 to 99, the sendNotification method lacks error handling for
notification failures. Wrap the notification sending logic in a try-catch block
to catch any exceptions thrown by notifier.notify(event). Log the error or
handle it appropriately to ensure that notification failures do not cause the
inquiry answer creation process to fail.



}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
Comment on lines +66 to +68

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error code ErrorCode.APPLICATION_SCHOOL_NOT_FOUND seems incorrect here. The method is trying to find a refund projection by examApplicationId. A more appropriate error code would be ErrorCode.EXAM_APPLICATION_NOT_FOUND (which is used in createApplicationVariable in the same file) or a new specific error code for "refund data not found". Using a misleading error code can make debugging more difficult.

                        () -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND));

return new RefundNotifyRequest(
projection.paymentKey(), projection.examDate(), projection.schoolName(),
projection.refundAmount(), projection.paymentMethod().getName(), projection.reason()
);
}
Comment on lines 64 to 73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify error code consistency and consider null safety.

The implementation looks correct, but there are a couple of points to verify:

  1. Error Code Mismatch: The method uses ErrorCode.APPLICATION_SCHOOL_NOT_FOUND when no refund data is found, but this seems semantically incorrect - it should probably be REFUND_NOT_FOUND or similar.

  2. Method Chain Safety: The call projection.paymentMethod().getName() assumes paymentMethod() is never null. Consider adding null safety if the payment method could be null.

Please verify the following:

Consider this improvement for null safety:

 return new RefundNotifyRequest(
     projection.paymentKey(), projection.examDate(), projection.schoolName(),
-    projection.refundAmount(), projection.paymentMethod().getName(), projection.reason()
+    projection.refundAmount(), 
+    projection.paymentMethod() != null ? projection.paymentMethod().getName() : "N/A", 
+    projection.reason()
 );

🏁 Script executed:

#!/bin/bash
# Check if REFUND_NOT_FOUND or similar error code exists in ErrorCode enum
ast-grep --pattern 'enum ErrorCode {
  $$$
}'

Length of output: 47


🏁 Script executed:

#!/bin/bash
set -e
# Locate the ErrorCode enum definition
file=$(fd ErrorCode.java | head -n1)
echo "Found ErrorCode enum in: $file"
# Dump its contents to list all defined error codes
sed -n '/enum ErrorCode/,/};/p' "$file"

Length of output: 6687


🏁 Script executed:

#!/bin/bash
set -e
# Locate and print the RefundNotifyProjection interface to check paymentMethod() return type
proj_file=$(fd RefundNotifyProjection.java | head -n1)
echo "=== $proj_file ==="
sed -n '1,200p' "$proj_file"

# Locate and inspect the Refund entity for the paymentMethod field nullability
entity_file=$(fd Refund.java | head -n1)
echo "=== $entity_file ==="
grep -R -n "paymentMethod" "$entity_file" -n
sed -n '1,200p' "$entity_file"

Length of output: 1198


Use a dedicated REFUND_NOT_FOUND error code and guard against null paymentMethod

Your implementation should throw a semantically correct error when no refund is found and only assume paymentMethod is non-null if your DB/schema guarantees it.

• In ErrorCode enum (src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java), add:

REFUND_NOT_FOUND(HttpStatus.NOT_FOUND, "환불 정보를 찾을 수 없습니다."),

• In NotifyVariableFactory#createRefundVariable (src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java), update the exception and (optionally) guard the paymentMethod name:

- .orElseThrow(() -> new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_NOT_FOUND));
+ .orElseThrow(() -> new CustomRuntimeException(ErrorCode.REFUND_NOT_FOUND));

return new RefundNotifyRequest(
    projection.paymentKey(),
    projection.examDate(),
    projection.schoolName(),
    projection.refundAmount(),
-   projection.paymentMethod().getName(),
+   projection.paymentMethod() != null
+       ? projection.paymentMethod().getName()
+       : "N/A",
    projection.reason()
);

• If paymentMethod is declared non-nullable in your JPA mapping and DB schema, you can omit the null check; otherwise keep a safe fallback or throw a different exception.

🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/notify/NotifyVariableFactory.java
lines 64-73, replace the current error code APPLICATION_SCHOOL_NOT_FOUND with a
new dedicated REFUND_NOT_FOUND error code that you must add to the ErrorCode
enum in src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java.
Additionally, add a null check for paymentMethod before accessing its name; if
paymentMethod can be null, either provide a safe fallback value or throw an
appropriate exception to avoid NullPointerException.


// 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public record RefundNotifyRequest(
String paymentKey,
LocalDate examDate,
String schoolName,
// String amount,
Integer refundAmount,
String paymentMethod,
String reason
) implements NotificationVariable {
Expand All @@ -30,7 +30,7 @@ public Map<String, String> toMap() {
"paymentKey", paymentKey,
"examDate", examDate.toString(),
"schoolName", schoolName,
"amount", String.valueOf(1000),
"refundAmount", String.valueOf(refundAmount),
"paymentMethod", paymentMethod,
"reason", reason
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ public class PaymentEventTxService {
private final PaymentTxEventFactory eventFactory;

@Transactional
public void publishSuccessEvent(List<Long> examApplicationIds, String orderId, int amount) {
public void publishSuccessEvent(List<Long> 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<Long> examApplicationIds, String orderId, int amount) {
public void publishFailureEvent(List<Long> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public PaymentPrepareResponse prepare(PreparePaymentRequest request) {
}

@Transactional
public void confirm(PaymentRequest request) {
public void confirm(Long userId, PaymentRequest request) {
List<ExamApplicationJpaEntity> applications = getValidApplications(request.applicationId());
String orderId = request.orderId();
int lunchAmount = amountCalculator.calculateLunchAmount(applications);
Expand All @@ -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);
Expand All @@ -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;

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public record PaymentContext(
List<Long> examSchoolIds,
String orderId,
Long userId,
Boolean isSuccess,
PaymentStatus status,
Integer totalAmount
Expand All @@ -14,10 +15,12 @@ public record PaymentContext(
public static PaymentContext ofSuccess(
List<Long> examSchoolIds,
String orderId,
Long userId,
Integer totalAmount) {
return new PaymentContext(
examSchoolIds,
orderId,
userId,
true,
PaymentStatus.DONE,
totalAmount
Expand All @@ -27,10 +30,12 @@ public static PaymentContext ofSuccess(
public static PaymentContext ofFailure(
List<Long> examSchoolIds,
String orderId,
Long userId,
Integer totalAmount) {
return new PaymentContext(
examSchoolIds,
orderId,
userId,
false,
PaymentStatus.ABORTED,
totalAmount
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +19,7 @@
public class PaymentTxEventListener {

private final TxFailureHandler<PaymentContext> paymentFailureHandler;
private final NotifyEventPublisher notifier;

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void afterRollbackHandler(PaymentTxEvent event) {
Expand All @@ -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);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -34,6 +38,8 @@ public void registerProfile(Long userId, SignUpProfileRequest request) {

user.grantUserRole();
syncUserInfoFromProfile(user, request);

sendNotification();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The sendNotification() method should accept the userId and pass it when creating the event. You will also need to update the call to this method on line 42 to pass the userId:

sendNotification(userId);

Suggested change
sendNotification();
sendNotification(userId);

}

@Transactional
Expand Down Expand Up @@ -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);
}
Comment on lines +75 to +78

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The sendNotification() method creates a NotificationEvent with a null userId because it uses NotificationEvent.create(NotificationStatus.SIGN_UP_SUCCESS). This will cause a NullPointerException in NotifyService, which needs the userId to fetch the user's phone number.

To fix this, the sendNotification method should accept the userId and pass it when creating the event.

Suggested change
private void sendNotification() {
NotificationEvent event = NotificationEvent.create(NotificationStatus.SIGN_UP_SUCCESS);
notifier.notify(event);
}
private void sendNotification(Long userId) {
NotificationEvent event = NotificationEvent.create(NotificationStatus.SIGN_UP_SUCCESS, userId, null);
notifier.notify(event);
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Loading