Skip to content
Open
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 @@ -14,6 +14,7 @@

/**
* 납부 상태 처리. 상태 변경과 납부자 명부 반영이 어긋나면 안 되므로 한 트랜잭션으로 묶는다.
* 서비스가 남기는 아웃박스 이벤트(납부 여부 변경)도 이 트랜잭션에 함께 커밋·롤백된다.
*/
@Component
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.ac.kookmin.stream.common.event;

import java.time.LocalDateTime;

/**
* 회원의 학생회비 납부가 확인되었다 — 운영진이 회원의 최신 납부 요청을 납부완료로 처리했다.
* occurredAt은 같은 회원의 납부·납부 취소가 재시도로 순서가 뒤바뀌어 도착할 때 늦은 쪽을 가려내는 데 쓴다.
*/
public record MemberFeePaid(Long memberId, Long studentTransferStatusId, LocalDateTime occurredAt)
implements DomainEvent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.ac.kookmin.stream.common.event;

import java.time.LocalDateTime;

/**
* 운영진이 회원의 납부완료를 정정했다 — 납부완료였던 최신 요청을 확인필요·납부확인중으로 바꿨다.
* occurredAt의 쓰임은 {@link MemberFeePaid}와 같다.
*/
public record MemberFeePaymentCanceled(Long memberId, Long studentTransferStatusId, LocalDateTime occurredAt)
implements DomainEvent {}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public interface StudentTransferStatusService {
/**
* 관리자 처리. 이미 처리된 요청도 다시 바꿀 수 있다. adminNote는 확인필요일 때만 남는다.
* 회원의 최신 요청만 바꿀 수 있고, 지난 요청이면 {@code FEE_REQUEST_NOT_LATEST}다.
* 납부 여부가 바뀌면(납부완료가 되거나 납부완료에서 벗어나면) 같은 트랜잭션에서 아웃박스에
* {@code MemberFeePaid} · {@code MemberFeePaymentCanceled}를 남긴다.
*/
StudentTransferStatus review(Long transferStatusId, TransferStatus status, String adminNote);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import java.util.stream.Collectors;
import kr.ac.kookmin.stream.common.BusinessException;
import kr.ac.kookmin.stream.common.DateUtil;
import kr.ac.kookmin.stream.common.OutboxWriter;
import kr.ac.kookmin.stream.common.PageOffset;
import kr.ac.kookmin.stream.common.PageResult;
import kr.ac.kookmin.stream.common.event.MemberFeePaid;
import kr.ac.kookmin.stream.common.event.MemberFeePaymentCanceled;
import kr.ac.kookmin.stream.welfare.domain.fee.domain.FeeErrorCode;
import kr.ac.kookmin.stream.welfare.domain.fee.domain.StudentTransferStatus;
import kr.ac.kookmin.stream.welfare.domain.fee.domain.TransferStatus;
Expand All @@ -24,6 +27,7 @@
class StudentTransferStatusServiceImpl implements StudentTransferStatusService {

private final StudentTransferStatusRepository studentTransferStatusRepository;
private final OutboxWriter outboxWriter;

// 조회 쿼리가 1개라 트랜잭션을 걸지 않는다(PR#67/#69 원칙 — 조회니까 붙이는 게 아니라 실제 이득이 있을 때만 붙인다).
@Override
Expand Down Expand Up @@ -68,8 +72,24 @@ public StudentTransferStatus review(Long transferStatusId, TransferStatus status
.orElseThrow(() -> new BusinessException(FeeErrorCode.FEE_REQUEST_NOT_FOUND));
requireLatest(transferStatus);

transferStatus.review(status, adminNote, LocalDateTime.now(DateUtil.KST));
return studentTransferStatusRepository.save(transferStatus);
boolean wasPaid = transferStatus.getStatus() == TransferStatus.PAID;
LocalDateTime now = LocalDateTime.now(DateUtil.KST);
transferStatus.review(status, adminNote, now);
StudentTransferStatus reviewed = studentTransferStatusRepository.save(transferStatus);

recordPaidChange(wasPaid, reviewed, now);
return reviewed;
}

// 회원의 납부 여부(최신 요청이 PAID인가)가 바뀔 때만 남긴다. 최신 요청만 처리하므로 이 요청의 변화가 곧 회원의 변화다.
// 상태 변경과 같은 트랜잭션에서 기록해서, 처리가 롤백되면 이벤트도 함께 사라진다(OutboxWriter는 트랜잭션 밖에서 부르면 실패한다).
private void recordPaidChange(boolean wasPaid, StudentTransferStatus reviewed, LocalDateTime now) {
boolean paid = reviewed.getStatus() == TransferStatus.PAID;
if (!wasPaid && paid) {
outboxWriter.record(new MemberFeePaid(reviewed.getMemberId(), reviewed.getId(), now));
} else if (wasPaid && !paid) {
outboxWriter.record(new MemberFeePaymentCanceled(reviewed.getMemberId(), reviewed.getId(), now));
}
}

// 지난 요청은 이력이라 바꾸지 않는다. 회원의 지금 상태는 최신 요청 하나뿐이고, 지난 요청을 되살리면
Expand Down
Loading