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 @@ -7,110 +7,115 @@
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import life.mosu.mosuserver.domain.application.ApplicationJpaEntity;
import life.mosu.mosuserver.domain.exam.ExamJpaEntity;
import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaEntity;
import life.mosu.mosuserver.domain.examapplication.ExamSubjectJpaEntity;
import life.mosu.mosuserver.domain.payment.PaymentJpaEntity;
import life.mosu.mosuserver.domain.refund.RefundJpaEntity;
import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse;
import life.mosu.mosuserver.presentation.application.dto.ExamApplicationResponse;
import life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationWithStatus;

public record ApplicationContext(
List<ApplicationJpaEntity> applications,
List<ExamApplicationJpaEntity> examApplications,
List<ExamApplicationWithStatus> examApplications,
Map<Long, ExamJpaEntity> examMap,
Map<Long, List<ExamSubjectJpaEntity>> subjectMap,
Map<Long, PaymentJpaEntity> paymentMap
Map<Long, PaymentJpaEntity> paymentMap,
Map<Long, RefundJpaEntity> refundMap
) {

public ApplicationContext(List<ApplicationJpaEntity> applications,
List<ExamApplicationJpaEntity> examApplications) {
this(applications, examApplications, Map.of(), Map.of(), Map.of());
public ApplicationContext(
List<ApplicationJpaEntity> applications,
List<ExamApplicationWithStatus> examApplications
) {
this(applications, examApplications, Map.of(), Map.of(), Map.of(), Map.of());
}

public ApplicationContext fetchExams(Function<List<Long>, List<ExamJpaEntity>> fetcher) {
List<Long> examIds = this.examApplications.stream().map(ExamApplicationJpaEntity::getExamId)
.distinct().toList();
Map<Long, ExamJpaEntity> newExamMap = fetcher.apply(examIds).stream()
.collect(Collectors.toMap(ExamJpaEntity::getId, Function.identity()));
return new ApplicationContext(this.applications, this.examApplications, newExamMap,
this.subjectMap, this.paymentMap);
Map<Long, ExamJpaEntity> newExamMap = fetcher.apply(
examApplications.stream()
.map(e -> e.examApplication().getExamId())
.distinct()
.toList()
).stream().collect(Collectors.toMap(ExamJpaEntity::getId, Function.identity()));

return new ApplicationContext(applications, examApplications, newExamMap, subjectMap, paymentMap, refundMap);
}

public ApplicationContext fetchSubjects(
Function<List<Long>, List<ExamSubjectJpaEntity>> fetcher) {
List<Long> examApplicationIds = this.examApplications.stream()
.map(ExamApplicationJpaEntity::getId).toList();
Map<Long, List<ExamSubjectJpaEntity>> newSubjectMap = fetcher.apply(examApplicationIds)
.stream()
.collect(Collectors.groupingBy(ExamSubjectJpaEntity::getExamApplicationId));
return new ApplicationContext(this.applications, this.examApplications, this.examMap,
newSubjectMap, this.paymentMap);
public ApplicationContext fetchSubjects(Function<List<Long>, List<ExamSubjectJpaEntity>> fetcher) {
Map<Long, List<ExamSubjectJpaEntity>> newSubjectMap = fetcher.apply(
examApplications.stream()
.map(e -> e.examApplication().getId())
.toList()
).stream().collect(Collectors.groupingBy(ExamSubjectJpaEntity::getExamApplicationId));

return new ApplicationContext(applications, examApplications, examMap, newSubjectMap, paymentMap, refundMap);
}

public ApplicationContext fetchPayments(Function<List<Long>, List<PaymentJpaEntity>> fetcher) {
List<Long> examApplicationIds = this.examApplications.stream()
.map(ExamApplicationJpaEntity::getId).toList();
Map<Long, PaymentJpaEntity> newPaymentMap = fetcher.apply(examApplicationIds).stream()
.collect(Collectors.toMap(
PaymentJpaEntity::getExamApplicationId,
Function.identity()));

return new ApplicationContext(this.applications, this.examApplications, this.examMap,
this.subjectMap, newPaymentMap);
Map<Long, PaymentJpaEntity> newPaymentMap = fetcher.apply(
examApplications.stream()
.map(e -> e.examApplication().getId())
.toList()
).stream().collect(Collectors.toMap(PaymentJpaEntity::getExamApplicationId, Function.identity()));

return new ApplicationContext(applications, examApplications, examMap, subjectMap, newPaymentMap, refundMap);
}

public ApplicationContext fetchRefunds(Function<List<Long>, List<RefundJpaEntity>> fetcher) {
Map<Long, RefundJpaEntity> newRefundMap = fetcher.apply(
examApplications.stream()
.map(e -> e.examApplication().getId())
.toList()
).stream().collect(Collectors.toMap(RefundJpaEntity::getExamApplicationId, Function.identity()));

return new ApplicationContext(applications, examApplications, examMap, subjectMap, paymentMap, newRefundMap);
}
Comment on lines +68 to 76
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential data loss if multiple refunds exist for the same exam application.

The toMap collector will throw an exception if there are duplicate keys (multiple refunds for the same examApplicationId). Consider using a merge function or grouping if multiple refunds per application are possible.

If multiple refunds are not possible by design, this is fine. Otherwise, consider:

-        ).stream().collect(Collectors.toMap(RefundJpaEntity::getExamApplicationId, Function.identity()));
+        ).stream().collect(Collectors.toMap(
+            RefundJpaEntity::getExamApplicationId, 
+            Function.identity(),
+            (existing, replacement) -> replacement // or choose appropriate merge strategy
+        ));
📝 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
public ApplicationContext fetchRefunds(Function<List<Long>, List<RefundJpaEntity>> fetcher) {
Map<Long, RefundJpaEntity> newRefundMap = fetcher.apply(
examApplications.stream()
.map(e -> e.examApplication().getId())
.toList()
).stream().collect(Collectors.toMap(RefundJpaEntity::getExamApplicationId, Function.identity()));
return new ApplicationContext(applications, examApplications, examMap, subjectMap, paymentMap, newRefundMap);
}
public ApplicationContext fetchRefunds(Function<List<Long>, List<RefundJpaEntity>> fetcher) {
Map<Long, RefundJpaEntity> newRefundMap = fetcher.apply(
examApplications.stream()
.map(e -> e.examApplication().getId())
.toList()
).stream().collect(Collectors.toMap(
RefundJpaEntity::getExamApplicationId,
Function.identity(),
(existing, replacement) -> replacement // or choose appropriate merge strategy
));
return new ApplicationContext(applications, examApplications, examMap, subjectMap, paymentMap, newRefundMap);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/application/ApplicationContext.java
around lines 68 to 76, the use of Collectors.toMap without a merge function can
cause exceptions if multiple RefundJpaEntity objects share the same
examApplicationId key. To fix this, either add a merge function to handle
duplicates or change the collection to group refunds by examApplicationId into a
list or another suitable structure, depending on whether multiple refunds per
application are allowed.


public List<ApplicationResponse> assemble() {
Map<Long, List<ExamApplicationResponse>> groupedExamResponses = this.examApplications.stream()
Map<Long, List<ExamApplicationResponse>> grouped = examApplications.stream()
.map(this::createExamApplicationResponse)
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

return this.applications.stream()
return applications.stream()
.map(app -> ApplicationResponse.of(app.getId(),
groupedExamResponses.getOrDefault(app.getId(), List.of())))
grouped.getOrDefault(app.getId(), List.of())))
.toList();
}

private Map.Entry<Long, ExamApplicationResponse> createExamApplicationResponse(
ExamApplicationJpaEntity examApp) throws RuntimeException {

ExamJpaEntity exam = this.examMap.get(examApp.getExamId());
if (exam == null) {
return null;
}
private Map.Entry<Long, ExamApplicationResponse> createExamApplicationResponse(ExamApplicationWithStatus item) {
ExamApplicationJpaEntity examApp = item.examApplication();
ExamJpaEntity exam = examMap.get(examApp.getExamId());
if (exam == null) return null;

List<ExamSubjectJpaEntity> subjectEntities = this.subjectMap.getOrDefault(examApp.getId(),
List.of());
Set<String> subjects = subjectEntities.stream().map(es -> es.getSubject().getSubjectName())
.collect(Collectors.toSet());
Set<String> subjects = subjectMap.getOrDefault(examApp.getId(), List.of()).stream()
.map(s -> s.getSubject().getSubjectName()).collect(Collectors.toSet());

PaymentJpaEntity payment = this.paymentMap.getOrDefault(examApp.getId(), null);
String paymentStatus = Optional.ofNullable(payment)
.map(p -> p.getPaymentStatus().name())
.orElse(null);
PaymentJpaEntity payment = paymentMap.get(examApp.getId());
RefundJpaEntity refund = refundMap.get(examApp.getId());

Integer totalAmount = Optional.ofNullable(payment)
.map(p -> p.getPaymentAmount().getTotalAmount())
.orElse(0);

String lunchName;
if (examApp.getIsLunchChecked()) {
lunchName = exam.getLunchName();
} else {
lunchName = null;
}
String lunchName = examApp.getIsLunchChecked() ? exam.getLunchName() : null;
String status = item.status();

ExamApplicationResponse response = ExamApplicationResponse.of(
examApp.getId(),
examApp.getCreatedAt(),
paymentStatus, totalAmount,
status,
totalAmount,
exam.getSchoolName(),
exam.getExamDate(),
subjects,
lunchName
);

return Map.entry(examApp.getApplicationId(), response);
}
Comment on lines +91 to 120
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

Consider utilizing the refund entity in the response.

The refund entity is fetched but not used in the response creation. Since the PR aims to integrate refund information, consider:

  1. Should the totalAmount reflect refunds (e.g., show negative amount for refunded applications)?
  2. Should refund details be included in the ExamApplicationResponse?

Let me check if ExamApplicationResponse has fields for refund information:


🏁 Script executed:

#!/bin/bash
# Check the structure of ExamApplicationResponse
fd -e java -x grep -l "ExamApplicationResponse" {} \; | xargs grep -A 10 "class ExamApplicationResponse\|record ExamApplicationResponse"

Length of output: 1448


Include refund information in the response creation

You’re currently fetching RefundJpaEntity refund but never using it. To fully support refunds, please decide on one of the following:

  • Adjust the reported amount by subtracting the refund:

    int paid = Optional.ofNullable(payment)
        .map(p -> p.getPaymentAmount().getTotalAmount())
        .orElse(0);
    int refunded = Optional.ofNullable(refund)
        .map(r -> r.getRefundAmount().getTotalAmount())
        .orElse(0);
    int netAmount = paid - refunded;

    and pass netAmount (or both paid and refunded) into the response.

  • Or extend ExamApplicationResponse to include refund fields:

    public record ExamApplicationResponse(
        Long examApplicationId,
        String createdAt,
        String status,
        Integer totalAmount,
        Integer refundedAmount,   // new
        Integer netAmount,        // new (optional)
        String schoolName,
        LocalDate examDate,
        Set<String> subjects,
        String lunchName
    ) { … }

Locations to update:

  • src/main/java/life/mosu/mosuserver/application/application/ApplicationContext.java (lines 91–120)
  • src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/application/application/ApplicationContext.java
lines 91 to 120, you are fetching the refund entity but not using it in the
response. To fix this, calculate the net amount by subtracting the refund amount
from the payment amount, then update the ExamApplicationResponse creation to
include this net amount or both paid and refunded amounts. Additionally, modify
the ExamApplicationResponse record in
src/main/java/life/mosu/mosuserver/presentation/application/dto/ExamApplicationResponse.java
to add fields for refundedAmount and netAmount, and update its constructor and
usage accordingly.

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaRepository;
import life.mosu.mosuserver.domain.examapplication.ExamSubjectJpaRepository;
import life.mosu.mosuserver.domain.payment.PaymentJpaRepository;
import life.mosu.mosuserver.domain.refund.RefundJpaRepository;
import life.mosu.mosuserver.global.processor.StepProcessor;
import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse;
import life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationWithStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
Expand All @@ -26,6 +28,7 @@ public class GetApplicationsStepProcessor implements
private final ExamApplicationJpaRepository examApplicationJpaRepository;
private final ExamJpaRepository examJpaRepository;
private final PaymentJpaRepository paymentJpaRepository;
private final RefundJpaRepository refundJpaRepository;

@Override
public List<ApplicationResponse> process(Long userId) {
Expand All @@ -37,13 +40,14 @@ public List<ApplicationResponse> process(Long userId) {
}

List<Long> applicationIds = applications.stream().map(ApplicationJpaEntity::getId).toList();
List<ExamApplicationJpaEntity> examApplications = examApplicationJpaRepository.findByApplicationIdIn(
List<ExamApplicationWithStatus> examApplications = examApplicationJpaRepository.findByApplicationIdIn(
applicationIds);
Comment on lines +43 to 44

Choose a reason for hiding this comment

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

high

The query in ExamApplicationJpaRepository already determines the application status by joining with the refund table. Therefore, .fetchRefunds(...) results in an extra database query that can be avoided. Removing this call will improve efficiency.

        return new ApplicationContext(applications, examApplications)
                .fetchExams(examJpaRepository::findByIdIn)
                .fetchSubjects(examSubjectJpaRepository::findByExamApplicationIdIn)
                .fetchPayments(paymentJpaRepository::findByExamApplicationIdIn)
                .assemble();


return new ApplicationContext(applications, examApplications)
.fetchExams(examJpaRepository::findByIdIn)
.fetchSubjects(examSubjectJpaRepository::findByExamApplicationIdIn)
.fetchPayments(paymentJpaRepository::findByExamApplicationIdIn)
.fetchRefunds(refundJpaRepository::findByExamApplicationIdIn)
.assemble();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import life.mosu.mosuserver.domain.examapplication.projection.ExamApplicationNotifyProjection;
import life.mosu.mosuserver.domain.examapplication.projection.ExamInfoWithExamNumberProjection;
import life.mosu.mosuserver.domain.examapplication.projection.ExamTicketInfoProjection;
import life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationWithStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand Down Expand Up @@ -71,7 +72,22 @@ Optional<ExamTicketInfoProjection> findExamTicketInfoProjectionById(

boolean existsByApplicationId(Long applicationId);

List<ExamApplicationJpaEntity> findByApplicationIdIn(List<Long> applicationIds);
@Query("""
SELECT new life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationWithStatus(
ea,
CASE
WHEN r.refundStatus = 'DONE' THEN '환불완료'
ELSE '결제완료'
END
)
Comment on lines +76 to +82

Choose a reason for hiding this comment

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

medium

The JPQL query returns presentation-layer strings ('환불완료', '결제완료'). Returning a boolean isRefunded and handling the mapping to display strings in the application layer would improve separation of concerns and maintainability.

    @Query("""
            SELECT new life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationWithStatus(
            ea,
            CASE
                 WHEN r.refundStatus = 'DONE' THEN true
                 ELSE false
            END
            )
            FROM ExamApplicationJpaEntity ea
            JOIN PaymentJpaEntity p ON p.examApplicationId = ea.id
            LEFT JOIN RefundJpaEntity r ON r.examApplicationId = ea.id
            WHERE p.paymentStatus = 'DONE'
            AND (r IS NULL OR r.refundStatus = 'DONE')
            AND ea.applicationId IN :applicationIds
            """)
    List<ExamApplicationWithStatus> findByApplicationIdIn(List<Long> applicationIds);

FROM ExamApplicationJpaEntity ea
JOIN PaymentJpaEntity p ON p.examApplicationId = ea.id
LEFT JOIN RefundJpaEntity r ON r.examApplicationId = ea.id
WHERE p.paymentStatus = 'DONE'
AND (r IS NULL OR r.refundStatus = 'DONE')
AND ea.applicationId IN :applicationIds
""")
List<ExamApplicationWithStatus> findByApplicationIdIn(List<Long> applicationIds);


@Query("""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package life.mosu.mosuserver.domain.refund;

import io.lettuce.core.dynamic.annotation.Param;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface RefundJpaRepository extends JpaRepository<RefundJpaEntity, Long> {

Expand Down Expand Up @@ -32,4 +33,11 @@ Optional<RefundNotifyProjection> findRefundByExamApplicationId(
@Param("examApplicationId") Long examApplicationId);

Optional<RefundJpaEntity> findByTransactionKey(String transactionKey);

@Query("""
SELECT r
FROM RefundJpaEntity r
WHERE r.examApplicationId IN :examApplicationIds
""")
List<RefundJpaEntity> findByExamApplicationIdIn(@Param("examApplicationIds") List<Long> examApplicationId);

Choose a reason for hiding this comment

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

medium

The parameter name examApplicationId is misleading for a List<Long>. Renaming it to examApplicationIds would improve readability.

Suggested change
List<RefundJpaEntity> findByExamApplicationIdIn(@Param("examApplicationIds") List<Long> examApplicationId);
List<RefundJpaEntity> findByExamApplicationIdIn(@Param("examApplicationIds") List<Long> examApplicationIds);

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public record ExamApplicationResponse(
Long examApplicationId,
String createdAt,
String paymentStatus,
String status,
Integer totalAmount,
String schoolName,
LocalDate examDate,
Expand All @@ -17,7 +17,7 @@ public record ExamApplicationResponse(
public static ExamApplicationResponse of(
Long examApplicationId,
String createdAt,
String paymentStatus,
String status,
Integer totalAmount,
String schoolName,
LocalDate examDate,
Expand All @@ -27,7 +27,7 @@ public static ExamApplicationResponse of(
return new ExamApplicationResponse(
examApplicationId,
createdAt,
paymentStatus,
status,
totalAmount,
schoolName,
examDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package life.mosu.mosuserver.presentation.examapplication.dto;

import life.mosu.mosuserver.domain.examapplication.ExamApplicationJpaEntity;

public record ExamApplicationWithStatus(
ExamApplicationJpaEntity examApplication,
String status
) { }