Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
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.payment.PaymentStatus;
import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse;
import life.mosu.mosuserver.presentation.application.dto.ExamApplicationResponse;

Expand Down Expand Up @@ -53,7 +52,6 @@ public ApplicationContext fetchPayments(Function<List<Long>, List<PaymentJpaEnti
List<Long> examApplicationIds = this.examApplications.stream()
.map(ExamApplicationJpaEntity::getId).toList();
Map<Long, PaymentJpaEntity> newPaymentMap = fetcher.apply(examApplicationIds).stream()
.filter(payment -> payment.getPaymentStatus().equals(PaymentStatus.DONE))
.collect(Collectors.toMap(
PaymentJpaEntity::getExamApplicationId,
Function.identity()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ public CreateApplicationResponse apply(Long userId, ApplicationRequest request)
Set<Subject> subjects = request.getSubjects();

validator.ExamDateNotPassed(examIds);
validator.RequestNoDuplicateExams(examIds);
validator.ExamIdsAndLunchSelection(request.examApplication());
validator.NoDuplicateApplication(userId, examIds);
validator.requestNoDuplicateExams(examIds);
validator.examIdsAndLunchSelection(request.examApplication());
validator.noDuplicateApplication(userId, examIds);

ApplicationJpaEntity application = request.toApplicationJpaEntity(userId);
ApplicationJpaEntity savedApplication = applicationJpaRepository.save(application);

Long applicationId = savedApplication.getId();

registerApplicationStepProcessor.process(
RegisterApplicationCommand.of(applicationId, request, subjects));
RegisterApplicationCommand.of(userId, applicationId, request, subjects));

saveExamTicketStepProcessor.process(
ApplicationProcessingContext.of(applicationId, request.admissionTicket()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
import life.mosu.mosuserver.presentation.application.dto.ApplicationRequest;

public record RegisterApplicationCommand(
Long userId,
Long applicationId,
ApplicationRequest applicationRequest,
Set<Subject> subjects
) {

public static RegisterApplicationCommand of(
Long userId,
Long applicationId,
ApplicationRequest applicationRequest,
Set<Subject> subjects
) {
return new RegisterApplicationCommand(applicationId, applicationRequest, subjects);
return new RegisterApplicationCommand(userId, applicationId, applicationRequest, subjects);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import life.mosu.mosuserver.global.processor.StepProcessor;
import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class GetApplicationsStepProcessor implements
Expand All @@ -29,6 +31,7 @@ public class GetApplicationsStepProcessor implements
public List<ApplicationResponse> process(Long userId) {

List<ApplicationJpaEntity> applications = applicationJpaRepository.findAllByUserId(userId);
log.info("applications info: {}", applications.size());
if (applications.isEmpty()) {
return List.of();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ public RegisterApplicationCommand process(RegisterApplicationCommand command) {
final List<ExamApplicationRequest> examApplicationRequests = command.applicationRequest()
.examApplication();
final Long applicationId = command.applicationId();
final Long userId = command.userId();

List<ExamApplicationJpaEntity> examApplicationEntities = examApplicationService.register(
RegisterExamApplicationEvent.of(examApplicationRequests,
applicationId)
applicationId, userId)
);

// 시험 신청 목록과 과목 multi-insert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ public class ApplicationValidator {
private final ExamJpaRepository examJpaRepository;
private final ApplicationJpaRepository applicationJpaRepository;

public void RequestNoDuplicateExams(List<Long> examIds) {
public void requestNoDuplicateExams(List<Long> examIds) {
Set<Long> examIdSet = new HashSet<>(examIds);
if (examIds.size() != examIdSet.size()) {
throw new CustomRuntimeException(ErrorCode.EXAM_DUPLICATED);
}
}

public void ExamIdsAndLunchSelection(List<ExamApplicationRequest> requests) {
public void examIdsAndLunchSelection(List<ExamApplicationRequest> requests) {
if (requests == null || requests.isEmpty()) {
throw new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND);
}
Expand All @@ -43,10 +43,10 @@ public void ExamIdsAndLunchSelection(List<ExamApplicationRequest> requests) {
throw new CustomRuntimeException(ErrorCode.EXAM_NOT_FOUND);
}

LunchSelection(requests, existingExams);
lunchSelection(requests, existingExams);
}

private void LunchSelection(List<ExamApplicationRequest> requests,
private void lunchSelection(List<ExamApplicationRequest> requests,
List<ExamJpaEntity> exams) {
Set<Long> examsWithoutLunch = exams.stream()
.filter(ExamJpaEntity::hasNotLunch)
Expand All @@ -61,7 +61,7 @@ private void LunchSelection(List<ExamApplicationRequest> requests,
}
}

public void NoDuplicateApplication(Long userId, List<Long> examIds) {
public void noDuplicateApplication(Long userId, List<Long> examIds) {
boolean alreadyApplied = applicationJpaRepository.existsByUserIdAndExamIds(userId, examIds);
if (alreadyApplied) {
throw new CustomRuntimeException(ErrorCode.APPLICATION_SCHOOL_DUPLICATED);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package life.mosu.mosuserver.application.examapplication;

import java.util.HashSet;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -15,6 +16,7 @@
import life.mosu.mosuserver.domain.examapplication.projection.ExamApplicationInfoProjection;
import life.mosu.mosuserver.domain.examapplication.projection.ExamTicketInfoProjection;
import life.mosu.mosuserver.domain.examapplication.service.ExamNumberGenerationService;
import life.mosu.mosuserver.domain.payment.PaymentJpaRepository;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;
import life.mosu.mosuserver.infra.persistence.s3.S3Service;
Expand All @@ -23,10 +25,12 @@
import life.mosu.mosuserver.presentation.examapplication.dto.ExamApplicationInfoResponse;
import life.mosu.mosuserver.presentation.examapplication.dto.UpdateSubjectRequest;
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 ExamApplicationService {
Expand All @@ -35,6 +39,7 @@ public class ExamApplicationService {
private final ApplicationJpaRepository applicationJpaRepository;
private final ExamSubjectJpaRepository examSubjectJpaRepository;
private final ExamNumberGenerationService examNumberGenerationService;
private final PaymentJpaRepository paymentJpaRepository;
private final S3Service s3Service;
private final FixedQuantityDiscountCalculator calculator;

Expand All @@ -47,62 +52,44 @@ public List<ExamApplicationJpaEntity> register(RegisterExamApplicationEvent even
}

@Transactional
public ExamApplicationInfoResponse updateSubjects(Long examApplicationId,
public void updateSubjects(Long userId, Long examApplicationId,
UpdateSubjectRequest request) {

examSubjectJpaRepository.deleteByExamApplicationId(examApplicationId);
validateUser(userId, examApplicationId);
examSubjectJpaRepository.deleteExamSubjectsWithDonePayment(examApplicationId);
List<ExamSubjectJpaEntity> examSubjects = request.toEntityList(examApplicationId);
examSubjectJpaRepository.saveAll(examSubjects);

ExamApplicationInfoProjection examApplicationInfo = examApplicationJpaRepository
.findExamApplicationInfoById(examApplicationId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND));

Integer totalAmount = examApplicationInfo.paymentAmount().getTotalAmount();
Integer discountAmount = getAppliedDiscountAmount(totalAmount);
AddressResponse address = AddressResponse.from(examApplicationInfo.address());
Set<String> subjectSet = new HashSet<>(request.subjects());

return ExamApplicationInfoResponse.of(
examApplicationInfo.examApplicationId(),
examApplicationInfo.paymentKey(),
examApplicationInfo.examDate(),
examApplicationInfo.schoolName(),
address,
subjectSet,
examApplicationInfo.lunchName(),
totalAmount,
discountAmount,
examApplicationInfo.paymentMethod().getName()
);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deleteExamApplication(Long examApplicationId) {
public void deleteExamApplication(Long userId, Long examApplicationId) {
validateUser(userId, examApplicationId);

ExamApplicationJpaEntity examApplication = examApplicationJpaRepository.findById(
examApplicationId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND));
Long applicationId = examApplication.getApplicationId();

examApplicationJpaRepository.deleteById(examApplicationId);
examApplicationJpaRepository.updateDeleteById(examApplicationId);

if (!examApplicationJpaRepository.existsByApplicationId(applicationId)) {
applicationJpaRepository.deleteById(applicationId);
applicationJpaRepository.deleteWithExamTicketById(applicationId);
}

examSubjectJpaRepository.deleteByExamApplicationId(examApplicationId);

}


@Transactional
public ExamTicketResponse getExamTicket(Long examApplicationId) {
public ExamTicketResponse getExamTicket(Long userId, Long examApplicationId) {
ExamTicketInfoProjection examTicketInfo = examApplicationJpaRepository.findExamTicketInfoProjectionById(
examApplicationId)
userId, examApplicationId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND));
() -> new CustomRuntimeException(ErrorCode.EXAM_RESOURCE_ACCESS_DENIED));

validateExamTicketOpenDate(examTicketInfo.examDate(), examTicketInfo.examNumber());

List<ExamSubjectJpaEntity> examSubjects = examSubjectJpaRepository.findByExamApplicationId(
examApplicationId);
Expand All @@ -112,31 +99,51 @@ public ExamTicketResponse getExamTicket(Long examApplicationId) {
.map(Subject::getSubjectName)
.toList();

String examTicketImgUrl = s3Service.getPreSignedUrl(examTicketInfo.s3Key());
String s3Key = examTicketInfo.s3Key();
String examTicketImgUrl = null;

if (s3Key != null) {
examTicketImgUrl = s3Service.getPreSignedUrl(s3Key);
}

return ExamTicketResponse.of(examTicketImgUrl, examTicketInfo.userName(),
examTicketInfo.birth(),
examTicketInfo.examNumber(), subjects, examTicketInfo.schoolName());

}

//TODO: 테스트 필요
public ExamApplicationInfoResponse getApplication(Long examApplicationId) {

public ExamApplicationInfoResponse getApplication(Long userId, Long examApplicationId,
Long applicationId) {
validateUser(userId, examApplicationId);

//상세 조회는 done 만 가능
// Integer examApplicationCount = paymentJpaRepository.countByExamApplicationId(
// examApplicationId);
List<ExamApplicationJpaEntity> examApplicationEntities = examApplicationJpaRepository.findByApplicationId(
applicationId);
Integer lunchCount = (int) examApplicationEntities.stream()
.filter(ExamApplicationJpaEntity::getIsLunchChecked)
.count();

ExamApplicationInfoProjection examApplicationInfo = examApplicationJpaRepository
.findExamApplicationInfoById(examApplicationId)
.findExamApplicationInfoById(userId, examApplicationId)
.orElseThrow(
() -> new CustomRuntimeException(ErrorCode.EXAM_APPLICATION_NOT_FOUND));

List<ExamSubjectJpaEntity> examSubjects =
examSubjectJpaRepository.findByExamApplicationId(examApplicationId);

Set<String> subjects = examSubjects.stream()
.map(examSubject -> examSubject.getSubject()
.getSubjectName())
.map(ExamSubjectJpaEntity::getSubjectName)
.collect(Collectors.toSet());
//totalAmount 는 Lunch 가격이 포함되었을 수도 있음
//totalAmount - Lunch 가격으로 getAppliedDiscountAmount() 메소드에 넣어야함.

Integer totalAmount = examApplicationInfo.paymentAmount().getTotalAmount();
Integer discountAmount = getAppliedDiscountAmount(totalAmount);
Integer discountAmount = getAppliedDiscountAmount(
lunchCount > 0 ? totalAmount - (9000 * lunchCount)
: totalAmount);
Comment on lines +145 to +146

Choose a reason for hiding this comment

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

medium

Using the magic number 9000 for the lunch price makes the code harder to maintain and prone to inconsistencies. This value should be externalized to a constant, perhaps in a configuration file or a shared constants class, to be used across the application. The DatabaseInitializer already defines a LUNCH_PRICE constant, and a similar approach should be adopted here.

                lunchCount > 0 ? totalAmount - (LUNCH_PRICE * lunchCount)
                        : totalAmount;


Integer paymentAmount =
examApplicationInfo.paymentAmount().getTotalAmount() + discountAmount;
Expand All @@ -155,7 +162,28 @@ public ExamApplicationInfoResponse getApplication(Long examApplicationId) {
);
}

private void validateUser(Long userId, Long examApplicationId) {
boolean check = examApplicationJpaRepository.existByUserIdAndExamApplicationId(
userId,
examApplicationId);

if (!check) {
throw new CustomRuntimeException(ErrorCode.USER_NOT_ACCESS_FORBIDDEN);
}
}

private int getAppliedDiscountAmount(Integer totalAmount) {
log.info("total amount: {}", totalAmount);
return calculator.getAppliedDiscountAmount(totalAmount);
}

private void validateExamTicketOpenDate(LocalDate examDate, String examNumber) {

LocalDateTime openDateTime = examDate.minusDays(3).atTime(8, 0);

Choose a reason for hiding this comment

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

medium

The values 3, 8, and 0 are used to define the exam ticket open date logic. These magic numbers make the code less readable. It would be better to extract them into named constants to clarify their purpose (e.g., EXAM_TICKET_OPEN_DAYS_BEFORE, EXAM_TICKET_OPEN_HOUR).

        LocalDateTime openDateTime = examDate.minusDays(EXAM_TICKET_OPEN_DAYS_BEFORE).atTime(EXAM_TICKET_OPEN_HOUR, 0);

LocalDateTime now = LocalDateTime.now();

Choose a reason for hiding this comment

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

medium

Using LocalDateTime.now() directly makes this method dependent on the system clock, which makes it difficult to test reliably. To improve testability, consider injecting a java.time.Clock instance and using LocalDateTime.now(clock). This allows you to control time within your tests.

        LocalDateTime now = LocalDateTime.now(clock); // Assuming a Clock instance is injected


if (examNumber == null || now.isBefore(openDateTime)) {
throw new CustomRuntimeException(ErrorCode.EXAM_TICKET_NOT_OPEN);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@

public record RegisterExamApplicationEvent(
List<TargetExam> targetExams,
Long applicationId
Long applicationId,
Long userId
) {

public static RegisterExamApplicationEvent of(
List<ExamApplicationRequest> examApplicationRequests,
Long applicationId
Long applicationId,
Long userId
) {
List<TargetExam> targetExams = examApplicationRequests.stream()
.map(request -> new TargetExam(request.examId(), request.isLunchChecked()))
.toList();
return new RegisterExamApplicationEvent(targetExams, applicationId);
return new RegisterExamApplicationEvent(targetExams, applicationId, userId);
}

public List<ExamApplicationJpaEntity> toEntity() {
return targetExams.stream()
.map(targetExam -> ExamApplicationJpaEntity.create(
applicationId,
userId,
targetExam.examId(),
targetExam.isLunchChecked()
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void confirm(PaymentRequest request) {
List<ExamApplicationJpaEntity> applications = getValidApplications(request.applicationId());
String orderId = request.orderId();
int lunchAmount = amountCalculator.calculateLunchAmount(applications);
Long applicationId = request.applicationId();
List<Long> examApplicationIds = applications.stream()
.map(ExamApplicationJpaEntity::getId)
.toList();
Expand All @@ -60,6 +61,7 @@ public void confirm(PaymentRequest request) {
ConfirmTossPaymentResponse tossResponse = tossProcessor.process(request);

List<PaymentJpaEntity> payments = paymentMapper.toEntities(
applicationId,
examApplicationIds,
tossResponse
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SoftDelete;

@Entity
@Table(name = "application")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SoftDelete
public class ApplicationJpaEntity extends BaseTimeEntity {

@Id
Expand Down
Loading