Skip to content

Commit 0da68eb

Browse files
Gopistolclaude
andauthored
fix: 이메일 전송 overview가 최신 배치 기준이 아닌 지원자별 누적 현황을 반영하도록 수정 (#393)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 814edce commit 0da68eb

File tree

5 files changed

+444
-39
lines changed

5 files changed

+444
-39
lines changed

src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormServiceImpl.java

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@
5151
import ddingdong.ddingdongBE.email.entity.EmailSendStatus;
5252
import ddingdong.ddingdongBE.email.service.EmailSendHistoryService;
5353
import java.time.LocalDate;
54-
import java.time.LocalDateTime;
5554
import java.util.List;
5655
import java.util.Map;
5756
import java.util.stream.Collectors;
5857
import lombok.RequiredArgsConstructor;
5958
import lombok.extern.slf4j.Slf4j;
60-
import org.jspecify.annotations.NonNull;
6159
import org.springframework.cache.annotation.CacheEvict;
6260
import org.springframework.cache.annotation.Caching;
6361
import org.springframework.context.ApplicationEventPublisher;
@@ -329,54 +327,34 @@ public Long resendApplicationResultEmail(EmailResendApplicationResultCommand com
329327

330328
@Override
331329
public EmailSendStatusOverviewQuery getEmailSendStatusOverviewByFormId(Long formId) {
332-
Map<FormApplicationStatus, Long> latestFormEmailSendHistoryIdByStatus =
333-
formEmailSendHistoryService.getLatestIdsByFormIdAndApplicationStatuses(
330+
List<EmailSendHistory> latestHistoriesPerApplication =
331+
emailSendHistoryService.getLatestPerApplicationByFormIdAndStatuses(
334332
formId, FormApplicationStatus.APPLICATION_RESULT_STATUSES);
335333

336-
List<EmailSendHistory> fetchedHistories = emailSendHistoryService.getAllFetchedByFormEmailSendHistoryIds(
337-
List.copyOf(latestFormEmailSendHistoryIdByStatus.values()));
334+
Map<FormApplicationStatus, EmailSendHistories> historiesByStatus =
335+
latestHistoriesPerApplication.stream()
336+
.collect(Collectors.groupingBy(
337+
history -> history.getFormEmailSendHistory().getFormApplicationStatus(),
338+
Collectors.collectingAndThen(Collectors.toList(), EmailSendHistories::new)
339+
));
338340

339-
Map<Long, EmailSendHistories> emailSendHistoriesByFormEmailSendHistoryId = fetchedHistories.stream()
340-
.collect(Collectors.groupingBy(
341-
history -> history.getFormEmailSendHistory().getId(),
342-
Collectors.collectingAndThen(Collectors.toList(), EmailSendHistories::new)
343-
));
344-
345-
List<EmailSendStatusOverviewInfoQuery> infos = getEmailSendStatusOverviewInfoQueries(
346-
FormApplicationStatus.APPLICATION_RESULT_STATUSES,
347-
latestFormEmailSendHistoryIdByStatus,
348-
emailSendHistoriesByFormEmailSendHistoryId);
349-
350-
return EmailSendStatusOverviewQuery.of(infos);
351-
}
352-
353-
private List<EmailSendStatusOverviewInfoQuery> getEmailSendStatusOverviewInfoQueries(
354-
List<FormApplicationStatus> statuses,
355-
Map<FormApplicationStatus, Long> latestFormEmailSendHistoryIdByStatus,
356-
Map<Long, EmailSendHistories> emailSendHistoriesByFormEmailSendHistoryId) {
357-
358-
return statuses.stream()
341+
List<EmailSendStatusOverviewInfoQuery> infos = FormApplicationStatus.APPLICATION_RESULT_STATUSES.stream()
359342
.map(status -> {
360-
Long formEmailSendHistoryId = latestFormEmailSendHistoryIdByStatus.get(status);
343+
EmailSendHistories histories = historiesByStatus.getOrDefault(
344+
status, new EmailSendHistories(List.of()));
361345

362-
if (formEmailSendHistoryId == null) {
346+
if (histories.getAll().isEmpty()) {
363347
return EmailSendStatusOverviewInfoQuery.empty(status);
364348
}
365349

366-
EmailSendHistories histories = emailSendHistoriesByFormEmailSendHistoryId.getOrDefault(
367-
formEmailSendHistoryId,
368-
new EmailSendHistories(List.of())
369-
);
370-
371-
EmailSendHistories latestHistoriesByFormApplication = histories.getLatestByFormApplication();
372-
LocalDateTime lastSentAt = latestHistoriesByFormApplication.getLastSentAt();
373-
374350
return EmailSendStatusOverviewInfoQuery.of(
375351
status,
376-
lastSentAt,
377-
latestHistoriesByFormApplication.getSuccessCount(),
378-
latestHistoriesByFormApplication.getFailCount());
352+
histories.getLastSentAt(),
353+
histories.getSuccessCount(),
354+
histories.getFailCount());
379355
}).toList();
356+
357+
return EmailSendStatusOverviewQuery.of(infos);
380358
}
381359

382360
private List<FormResultSendingEmailInfo> createPendingEmailInfos(

src/main/java/ddingdong/ddingdongBE/email/repository/EmailSendHistoryRepository.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,22 @@ List<EmailSendHistory> findLatestByFormIdAndStatusesAndApplicationStatus(
4040
@Param("applicationStatus") FormApplicationStatus applicationStatus
4141
);
4242

43+
@Query("""
44+
select esh
45+
from EmailSendHistory esh
46+
join fetch esh.formEmailSendHistory fesh
47+
where esh.id in (
48+
select max(esh2.id)
49+
from EmailSendHistory esh2
50+
join esh2.formEmailSendHistory fesh2
51+
where fesh2.form.id = :formId
52+
and fesh2.formApplicationStatus in :statuses
53+
group by esh2.formApplication.id, fesh2.formApplicationStatus
54+
)
55+
""")
56+
List<EmailSendHistory> findLatestPerApplicationByFormIdAndApplicationStatuses(
57+
@Param("formId") Long formId,
58+
@Param("statuses") List<FormApplicationStatus> statuses
59+
);
4360

4461
}

src/main/java/ddingdong/ddingdongBE/email/service/EmailSendHistoryService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,10 @@ public List<EmailSendHistory> getAllFetchedByFormEmailSendHistoryIds(List<Long>
9292
return emailSendHistoryRepository.findAllFetchedByFormEmailSendHistoryIdIn(ids);
9393
}
9494

95+
public List<EmailSendHistory> getLatestPerApplicationByFormIdAndStatuses(
96+
Long formId, List<FormApplicationStatus> statuses) {
97+
return emailSendHistoryRepository.findLatestPerApplicationByFormIdAndApplicationStatuses(
98+
formId, statuses);
99+
}
100+
95101
}

src/test/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormServiceImplTest.java

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,185 @@ void usesLatestFormEmailSendHistoryPerStatus() {
608608
}
609609

610610

611+
@DisplayName("재전송 후 overview는 가장 최신 배치만이 아닌 지원자별 누적 현황을 반영한다")
612+
@Test
613+
void overviewReflectsAccumulatedStatusAcrossAllBatchesAfterResend() {
614+
// given
615+
User savedUser = userRepository.save(UserFixture.createClubUser());
616+
Club savedClub = clubRepository.save(ClubFixture.createClub(savedUser));
617+
Form savedForm = formRepository.save(FormFixture.createForm(savedClub));
618+
619+
FormEmailSendHistory initialBatch = formEmailSendHistoryRepository.save(
620+
FormEmailSendHistoryFixture.createFinalPass(savedForm));
621+
622+
// 초기 발송: 3명 성공, 2명 실패
623+
FormApplication successApplication1 = formApplicationRepository.save(
624+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FINAL_PASS));
625+
FormApplication successApplication2 = formApplicationRepository.save(
626+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FINAL_PASS));
627+
FormApplication successApplication3 = formApplicationRepository.save(
628+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FINAL_PASS));
629+
FormApplication failApplication1 = formApplicationRepository.save(
630+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FINAL_PASS));
631+
FormApplication failApplication2 = formApplicationRepository.save(
632+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FINAL_PASS));
633+
634+
emailSendHistoryRepository.save(EmailSendHistoryFixture.deliverySuccess(successApplication1, initialBatch));
635+
emailSendHistoryRepository.save(EmailSendHistoryFixture.deliverySuccess(successApplication2, initialBatch));
636+
emailSendHistoryRepository.save(EmailSendHistoryFixture.deliverySuccess(successApplication3, initialBatch));
637+
emailSendHistoryRepository.save(EmailSendHistoryFixture.permanentFailureWithFormEmailSendHistory(failApplication1, initialBatch));
638+
emailSendHistoryRepository.save(EmailSendHistoryFixture.permanentFailureWithFormEmailSendHistory(failApplication2, initialBatch));
639+
640+
// 재전송: 실패한 2명 중 1명 성공, 1명 여전히 실패
641+
FormEmailSendHistory resendBatch = formEmailSendHistoryRepository.save(
642+
FormEmailSendHistoryFixture.createFinalPass(savedForm));
643+
644+
emailSendHistoryRepository.save(EmailSendHistoryFixture.deliverySuccess(failApplication1, resendBatch));
645+
emailSendHistoryRepository.save(EmailSendHistoryFixture.permanentFailureWithFormEmailSendHistory(failApplication2, resendBatch));
646+
647+
// when
648+
EmailSendStatusOverviewQuery result =
649+
facadeCentralFormService.getEmailSendStatusOverviewByFormId(savedForm.getId());
650+
651+
// then: 재전송 배치(2건)가 아닌 전체 5명의 누적 현황으로 집계돼야 한다
652+
EmailSendStatusOverviewInfoQuery finalPassOverview =
653+
result.emailSendStatusOverviewInfoQueries().stream()
654+
.filter(info -> info.formApplicationStatus() == FormApplicationStatus.FINAL_PASS)
655+
.findFirst()
656+
.orElseThrow();
657+
658+
assertThat(finalPassOverview.successCount()).isEqualTo(4); // 기존 3명 + 재전송 성공 1명
659+
assertThat(finalPassOverview.failCount()).isEqualTo(1); // 재전송 후에도 실패한 1명
660+
}
661+
662+
@DisplayName("재전송을 여러 번 반복해도 지원자별 가장 최신 상태로 집계한다")
663+
@Test
664+
void overviewReflectsLatestStatusAfterMultipleResends() {
665+
// given
666+
User savedUser = userRepository.save(UserFixture.createClubUser());
667+
Club savedClub = clubRepository.save(ClubFixture.createClub(savedUser));
668+
Form savedForm = formRepository.save(FormFixture.createForm(savedClub));
669+
670+
FormEmailSendHistory batch1 = formEmailSendHistoryRepository.save(
671+
FormEmailSendHistoryFixture.createFirstPass(savedForm));
672+
673+
FormApplication application = formApplicationRepository.save(
674+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FIRST_PASS));
675+
676+
// 1차: 실패
677+
emailSendHistoryRepository.save(
678+
EmailSendHistoryFixture.permanentFailureWithFormEmailSendHistory(application, batch1));
679+
680+
// 2차 재전송: 또 실패
681+
FormEmailSendHistory batch2 = formEmailSendHistoryRepository.save(
682+
FormEmailSendHistoryFixture.createFirstPass(savedForm));
683+
emailSendHistoryRepository.save(
684+
EmailSendHistoryFixture.permanentFailureWithFormEmailSendHistory(application, batch2));
685+
686+
// 3차 재전송: 성공
687+
FormEmailSendHistory batch3 = formEmailSendHistoryRepository.save(
688+
FormEmailSendHistoryFixture.createFirstPass(savedForm));
689+
emailSendHistoryRepository.save(
690+
EmailSendHistoryFixture.deliverySuccess(application, batch3));
691+
692+
// when
693+
EmailSendStatusOverviewQuery result =
694+
facadeCentralFormService.getEmailSendStatusOverviewByFormId(savedForm.getId());
695+
696+
// then: 3번째 배치(최종 성공)가 지원자의 최신 상태
697+
EmailSendStatusOverviewInfoQuery firstPassOverview =
698+
result.emailSendStatusOverviewInfoQueries().stream()
699+
.filter(info -> info.formApplicationStatus() == FormApplicationStatus.FIRST_PASS)
700+
.findFirst()
701+
.orElseThrow();
702+
703+
assertThat(firstPassOverview.successCount()).isEqualTo(1);
704+
assertThat(firstPassOverview.failCount()).isZero();
705+
}
706+
707+
@DisplayName("재전송 없이 아직 한 번도 이메일을 보내지 않은 status는 빈 현황을 반환한다")
708+
@Test
709+
void overviewReturnsEmptyForStatusWithNoEmailHistory() {
710+
// given
711+
User savedUser = userRepository.save(UserFixture.createClubUser());
712+
Club savedClub = clubRepository.save(ClubFixture.createClub(savedUser));
713+
Form savedForm = formRepository.save(FormFixture.createForm(savedClub));
714+
715+
// FIRST_PASS만 이메일 전송, FINAL_PASS는 전송 없음
716+
FormEmailSendHistory firstPassBatch = formEmailSendHistoryRepository.save(
717+
FormEmailSendHistoryFixture.createFirstPass(savedForm));
718+
FormApplication firstPassApplication = formApplicationRepository.save(
719+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FIRST_PASS));
720+
emailSendHistoryRepository.save(
721+
EmailSendHistoryFixture.deliverySuccess(firstPassApplication, firstPassBatch));
722+
723+
// when
724+
EmailSendStatusOverviewQuery result =
725+
facadeCentralFormService.getEmailSendStatusOverviewByFormId(savedForm.getId());
726+
727+
// then
728+
EmailSendStatusOverviewInfoQuery finalPassOverview =
729+
result.emailSendStatusOverviewInfoQueries().stream()
730+
.filter(info -> info.formApplicationStatus() == FormApplicationStatus.FINAL_PASS)
731+
.findFirst()
732+
.orElseThrow();
733+
734+
assertThat(finalPassOverview.successCount()).isZero();
735+
assertThat(finalPassOverview.failCount()).isZero();
736+
assertThat(finalPassOverview.lastSentAt()).isNull();
737+
}
738+
739+
@DisplayName("FIRST_PASS 재전송이 FINAL_PASS 집계에 영향을 주지 않는다")
740+
@Test
741+
void overviewAggregatesEachStatusIndependently() {
742+
// given
743+
User savedUser = userRepository.save(UserFixture.createClubUser());
744+
Club savedClub = clubRepository.save(ClubFixture.createClub(savedUser));
745+
Form savedForm = formRepository.save(FormFixture.createForm(savedClub));
746+
747+
FormEmailSendHistory firstPassBatch = formEmailSendHistoryRepository.save(
748+
FormEmailSendHistoryFixture.createFirstPass(savedForm));
749+
FormEmailSendHistory finalPassBatch = formEmailSendHistoryRepository.save(
750+
FormEmailSendHistoryFixture.createFinalPass(savedForm));
751+
752+
FormApplication firstPassApplication = formApplicationRepository.save(
753+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FIRST_PASS));
754+
FormApplication finalPassApplication = formApplicationRepository.save(
755+
FormApplicationFixture.create(savedForm, FormApplicationStatus.FINAL_PASS));
756+
757+
emailSendHistoryRepository.save(
758+
EmailSendHistoryFixture.permanentFailureWithFormEmailSendHistory(firstPassApplication, firstPassBatch));
759+
emailSendHistoryRepository.save(
760+
EmailSendHistoryFixture.deliverySuccess(finalPassApplication, finalPassBatch));
761+
762+
// FIRST_PASS 재전송 추가
763+
FormEmailSendHistory firstPassResendBatch = formEmailSendHistoryRepository.save(
764+
FormEmailSendHistoryFixture.createFirstPass(savedForm));
765+
emailSendHistoryRepository.save(
766+
EmailSendHistoryFixture.deliverySuccess(firstPassApplication, firstPassResendBatch));
767+
768+
// when
769+
EmailSendStatusOverviewQuery result =
770+
facadeCentralFormService.getEmailSendStatusOverviewByFormId(savedForm.getId());
771+
772+
// then
773+
EmailSendStatusOverviewInfoQuery firstPassOverview =
774+
result.emailSendStatusOverviewInfoQueries().stream()
775+
.filter(info -> info.formApplicationStatus() == FormApplicationStatus.FIRST_PASS)
776+
.findFirst()
777+
.orElseThrow();
778+
EmailSendStatusOverviewInfoQuery finalPassOverview =
779+
result.emailSendStatusOverviewInfoQueries().stream()
780+
.filter(info -> info.formApplicationStatus() == FormApplicationStatus.FINAL_PASS)
781+
.findFirst()
782+
.orElseThrow();
783+
784+
assertThat(firstPassOverview.successCount()).isEqualTo(1);
785+
assertThat(firstPassOverview.failCount()).isZero();
786+
assertThat(finalPassOverview.successCount()).isEqualTo(1);
787+
assertThat(finalPassOverview.failCount()).isZero();
788+
}
789+
611790
@DisplayName("같은 지원자에게 여러 번 이메일을 보낸 경우 지원자별 최신 전송 결과만 집계한다")
612791
@Test
613792
void countsLatestEmailSendHistoryPerFormApplication() {

0 commit comments

Comments
 (0)