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 @@ -13,6 +13,7 @@
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MultipartException;

import com.dnd.moddo.common.logging.DiscordMessage;
import com.dnd.moddo.common.logging.ErrorNotifier;
Expand Down Expand Up @@ -87,6 +88,14 @@ public ResponseEntity<ErrorResponse> handleConstraintViolation(
.body(new ErrorResponse(400, e.getMessage()));
}

@ExceptionHandler(MultipartException.class)
public ResponseEntity<ErrorResponse> handleMultipartException(MultipartException exception) {
LoggingUtils.warn(exception);

return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "잘못된 multipart 요청입니다."));
}

@ExceptionHandler({ModdoException.class})
public ResponseEntity<ErrorResponse> handleDefineException(ModdoException exception) {
LoggingUtils.warn(exception);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/dnd/moddo/common/logging/LoggingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MultipartException;

import com.dnd.moddo.common.exception.ModdoException;

Expand All @@ -24,6 +25,11 @@ public static void warn(MethodArgumentTypeMismatchException exception) {
log.warn(message + "\n \t {}", exception);
}

public static void warn(MultipartException exception) {
String message = getExceptionMessage(exception.getMessage());
log.warn(message + "\n \t {}", exception);
}

private static String getExceptionMessage(String message) {
if (message == null || message.isBlank()) {
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import com.dnd.moddo.event.application.impl.ExpenseDeleter;
import com.dnd.moddo.event.application.impl.ExpenseReader;
import com.dnd.moddo.event.application.impl.ExpenseUpdater;
import com.dnd.moddo.event.application.impl.PaymentRequestReader;
import com.dnd.moddo.event.application.impl.SettlementReader;
import com.dnd.moddo.event.application.impl.SettlementValidator;
import com.dnd.moddo.event.domain.expense.Expense;
import com.dnd.moddo.event.domain.expense.exception.ExpenseModificationLockedException;
import com.dnd.moddo.event.domain.settlement.Settlement;
import com.dnd.moddo.event.presentation.request.ExpenseImageRequest;
import com.dnd.moddo.event.presentation.request.ExpenseRequest;
Expand All @@ -33,8 +35,11 @@ public class CommandExpenseService {
private final CommandMemberExpenseService commandMemberExpenseService;
private final SettlementReader settlementReader;
private final SettlementValidator settlementValidator;
private final PaymentRequestReader paymentRequestReader;

public ExpensesResponse createExpenses(Long groupId, ExpensesRequest request) {
validateExpenseModifiable(groupId);

List<ExpenseResponse> expenses = request.expenses()
.stream()
.map(e -> createExpense(groupId, e))
Expand All @@ -58,6 +63,7 @@ public ExpenseResponse update(Long userId, Long expenseId, Long settlementId, Ex

Settlement settlement = settlementReader.read(settlementId);
settlementValidator.checkSettlementAuthor(settlement, userId);
validateExpenseModifiable(settlementId);

expense = expenseUpdater.update(expenseId, request);
List<MemberExpenseResponse> memberExpenseResponses = commandMemberExpenseService.update(expenseId,
Expand All @@ -70,6 +76,7 @@ public ExpenseResponse update(Long userId, Long expenseId, Long settlementId, Ex
public void updateImgUrl(Long userId, Long groupId, Long expenseId, ExpenseImageRequest request) {
Settlement settlement = settlementReader.read(groupId);
settlementValidator.checkSettlementAuthor(settlement, userId);
validateExpenseModifiable(groupId);
expenseUpdater.updateImgUrl(expenseId, request);
}

Expand All @@ -80,9 +87,16 @@ public void delete(Long userId, Long expenseId, Long settlementId) {

Settlement settlement = settlementReader.read(settlementId);
settlementValidator.checkSettlementAuthor(settlement, userId);
validateExpenseModifiable(settlementId);

commandMemberExpenseService.deleteAllByExpenseId(expenseId);
expenseDeleter.delete(expense);
cacheEvictor.evictSettlementHeader(settlementId);
}

private void validateExpenseModifiable(Long settlementId) {
if (paymentRequestReader.existsBySettlementId(settlementId)) {
throw new ExpenseModificationLockedException(settlementId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.transaction.annotation.Transactional;

import com.dnd.moddo.event.domain.member.Member;
import com.dnd.moddo.event.domain.member.ExpenseRole;
import com.dnd.moddo.event.domain.member.exception.MemberNotFoundException;
import com.dnd.moddo.event.domain.member.type.MemberSortType;
import com.dnd.moddo.event.infrastructure.MemberQueryRepository;
Expand Down Expand Up @@ -45,6 +46,10 @@ public boolean existsUnpaidMember(Long settlementId) {
return memberRepository.existsBySettlementIdAndIsPaidFalse(settlementId);
}

public boolean existsPaidParticipant(Long settlementId) {
return memberRepository.existsBySettlementIdAndRoleAndIsPaidTrue(settlementId, ExpenseRole.PARTICIPANT);
}

public List<Member> findAssignedMembersBySettlementId(Long settlementId) {
return findAllBySettlementId(settlementId).stream()
.filter(Member::isAssigned)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
public class PaymentRequestReader {
private final PaymentRequestRepository paymentRequestRepository;
private final MemberExpenseReader memberExpenseReader;
private final MemberReader memberReader;

public PaymentRequestsResponse findByTargetUserId(Long targetUserId) {
List<PaymentRequest> paymentRequests = paymentRequestRepository.findByTargetUserId(targetUserId)
Expand Down Expand Up @@ -63,7 +64,8 @@ public PaymentRequestsResponse findByTargetUserId(Long targetUserId) {
}

public boolean existsBySettlementId(Long settlementId) {
return paymentRequestRepository.existsBySettlementId(settlementId);
return paymentRequestRepository.existsBySettlementId(settlementId)
|| memberReader.existsPaidParticipant(settlementId);
}

public Map<Long, PaymentRequestSummaryResponse> findLatestRequestByMemberId(Long settlementId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.moddo.event.domain.expense.exception;

import org.springframework.http.HttpStatus;

import com.dnd.moddo.common.exception.ModdoException;

public class ExpenseModificationLockedException extends ModdoException {
public ExpenseModificationLockedException(Long settlementId) {
super(HttpStatus.FORBIDDEN, "입금 확인 요청 또는 입금 완료 상태가 있어 지출내역을 수정할 수 없습니다. (Settlement ID: " + settlementId + ")");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import com.dnd.moddo.event.domain.member.Member;
import com.dnd.moddo.event.domain.member.exception.MemberNotFoundException;
import com.dnd.moddo.event.domain.member.ExpenseRole;

public interface MemberRepository extends JpaRepository<Member, Long> {

Expand Down Expand Up @@ -39,6 +40,18 @@ select count(gm) > 0
""")
boolean existsBySettlementIdAndIsPaidFalse(@Param("settlementId") Long settlementId);

@Query("""
select count(gm) > 0
from Member gm
where gm.settlement.id = :settlementId
and gm.role = :role
and gm.isPaid = true
""")
boolean existsBySettlementIdAndRoleAndIsPaidTrue(
@Param("settlementId") Long settlementId,
@Param("role") ExpenseRole role
);

default Member getById(Long id) {
return findById(id)
.orElseThrow(() -> new MemberNotFoundException(id));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dnd.moddo.common.exception;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartException;

import com.dnd.moddo.common.logging.DiscordMessage;
import com.dnd.moddo.common.logging.ErrorNotifier;

@ExtendWith(MockitoExtension.class)
class GlobalExceptionHandlerTest {

@InjectMocks
private GlobalExceptionHandler globalExceptionHandler;

@Mock
private ErrorNotifier errorNotifier;

@Test
void givenInvalidMultipartRequest_thenReturnBadRequestWithoutErrorNotification() {
// given
MultipartException exception = new MultipartException("Failed to parse multipart servlet request");

// when
ResponseEntity<ErrorResponse> response = globalExceptionHandler.handleMultipartException(exception);

// then
assertThat(response.getStatusCode().value()).isEqualTo(400);
assertThat(response.getBody())
.isEqualTo(new ErrorResponse(400, "잘못된 multipart 요청입니다."));
then(errorNotifier).should(never()).notifyError(any(DiscordMessage.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import com.dnd.moddo.event.application.impl.ExpenseDeleter;
import com.dnd.moddo.event.application.impl.ExpenseReader;
import com.dnd.moddo.event.application.impl.ExpenseUpdater;
import com.dnd.moddo.event.application.impl.PaymentRequestReader;
import com.dnd.moddo.event.application.impl.SettlementReader;
import com.dnd.moddo.event.application.impl.SettlementValidator;
import com.dnd.moddo.event.domain.expense.Expense;
import com.dnd.moddo.event.domain.expense.exception.ExpenseModificationLockedException;
import com.dnd.moddo.event.domain.expense.exception.ExpenseNotFoundException;
import com.dnd.moddo.event.domain.expense.exception.ExpenseNotSettlementException;
import com.dnd.moddo.event.domain.settlement.Settlement;
Expand Down Expand Up @@ -53,6 +55,8 @@ class CommandExpenseServiceTest {
@Mock
private SettlementValidator settlementValidator;
@Mock
private PaymentRequestReader paymentRequestReader;
@Mock
private CommandMemberExpenseService commandMemberExpenseService;
@Mock
private CacheEvictor cacheEvictor;
Expand Down Expand Up @@ -117,6 +121,7 @@ void updateSuccess() {
when(expenseReader.findByExpenseId(eq(expenseId))).thenReturn(mockExpense);
when(settlementReader.read(eq(groupId))).thenReturn(mockSettlement);
doNothing().when(settlementValidator).checkSettlementAuthor(eq(mockSettlement), eq(userId));
when(paymentRequestReader.existsBySettlementId(groupId)).thenReturn(false);

when(expenseUpdater.update(eq(expenseId), eq(expenseRequest))).thenReturn(mockExpense);
when(commandMemberExpenseService.update(eq(expenseId), any())).thenReturn(
Expand All @@ -128,10 +133,36 @@ void updateSuccess() {
//then
assertThat(response).isNotNull();
verify(mockExpense, times(1)).validateSettlement(groupId);
verify(paymentRequestReader, times(1)).existsBySettlementId(groupId);
verify(expenseUpdater, times(1)).update(expenseId, expenseRequest);
verify(cacheEvictor, times(1)).evictSettlementHeader(groupId);
}

@DisplayName("입금 확인 요청 또는 입금 완료 참여자가 있으면 지출내역을 수정할 수 없다.")
@Test
void updateLockedByPaymentProgress() {
// given
Long userId = 1L;
Long settlementId = 1L;
Long expenseId = 10L;
ExpenseRequest expenseRequest = mock(ExpenseRequest.class);
Settlement mockSettlement = new Settlement(settlementId, userId, "정산", null, null, null, null, null, null, 1L,
"code");
Expense mockExpense = mock(Expense.class);

when(expenseReader.findByExpenseId(expenseId)).thenReturn(mockExpense);
when(settlementReader.read(settlementId)).thenReturn(mockSettlement);
when(paymentRequestReader.existsBySettlementId(settlementId)).thenReturn(true);

// when & then
assertThatThrownBy(() -> commandExpenseService.update(userId, expenseId, settlementId, expenseRequest))
.isInstanceOf(ExpenseModificationLockedException.class);

verify(expenseUpdater, never()).update(anyLong(), any());
verify(commandMemberExpenseService, never()).update(anyLong(), any());
verify(cacheEvictor, never()).evictSettlementHeader(anyLong());
}

@DisplayName("업데이트하려는 지출 내역을 찾을 수 없을때 예외를 발생시킨다.")
@Test
void updateNotFound() {
Expand Down Expand Up @@ -164,6 +195,7 @@ void deleteSuccess() {
when(expenseReader.findByExpenseId(eq(expenseId))).thenReturn(mockExpense);
when(settlementReader.read(eq(settlementId))).thenReturn(mockSettlement);
doNothing().when(settlementValidator).checkSettlementAuthor(eq(mockSettlement), eq(userId));
when(paymentRequestReader.existsBySettlementId(settlementId)).thenReturn(false);

doNothing().when(commandMemberExpenseService).deleteAllByExpenseId(eq(expenseId));
doNothing().when(expenseDeleter).delete(eq(mockExpense));
Expand All @@ -173,11 +205,36 @@ void deleteSuccess() {

//then
verify(mockExpense, times(1)).validateSettlement(settlementId);
verify(paymentRequestReader, times(1)).existsBySettlementId(settlementId);
verify(commandMemberExpenseService, times(1)).deleteAllByExpenseId(eq(expenseId));
verify(expenseDeleter, times(1)).delete(eq(mockExpense));
verify(cacheEvictor, times(1)).evictSettlementHeader(settlementId);
}

@DisplayName("입금 확인 요청 또는 입금 완료 참여자가 있으면 지출내역을 삭제할 수 없다.")
@Test
void deleteLockedByPaymentProgress() {
// given
Long userId = 1L;
Long settlementId = 1L;
Long expenseId = 10L;
Settlement mockSettlement = new Settlement(settlementId, userId, "정산", null, null, null, null, null, null, 1L,
"code");
Expense mockExpense = mock(Expense.class);

when(expenseReader.findByExpenseId(expenseId)).thenReturn(mockExpense);
when(settlementReader.read(settlementId)).thenReturn(mockSettlement);
when(paymentRequestReader.existsBySettlementId(settlementId)).thenReturn(true);

// when & then
assertThatThrownBy(() -> commandExpenseService.delete(userId, expenseId, settlementId))
.isInstanceOf(ExpenseModificationLockedException.class);

verify(commandMemberExpenseService, never()).deleteAllByExpenseId(anyLong());
verify(expenseDeleter, never()).delete(any());
verify(cacheEvictor, never()).evictSettlementHeader(anyLong());
}

@DisplayName("삭제하려는 지출내역이 해당 정산에 속하지 않으면 예외가 발생한다.")
@Test
void deleteNotSettlement() {
Expand Down Expand Up @@ -251,13 +308,15 @@ void updateImgUrlSuccess() {

when(settlementReader.read(groupId)).thenReturn(mockSettlement);
doNothing().when(settlementValidator).checkSettlementAuthor(mockSettlement, userId);
when(paymentRequestReader.existsBySettlementId(groupId)).thenReturn(false);

// when
commandExpenseService.updateImgUrl(userId, groupId, expenseId, request);

// then
verify(settlementReader, times(1)).read(groupId);
verify(settlementValidator, times(1)).checkSettlementAuthor(mockSettlement, userId);
verify(paymentRequestReader, times(1)).existsBySettlementId(groupId);
verify(expenseUpdater, times(1)).updateImgUrl(expenseId, request);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void findByTargetUserId() {
}

@Test
@DisplayName("정산에 생성된 입금 확인 요청이 있는지 확인할 수 있다.")
@DisplayName("정산에 생성된 입금 확인 요청 또는 입금 완료 참여자가 있는지 확인할 수 있다.")
void existsBySettlementId() {
when(paymentRequestReader.existsBySettlementId(1L)).thenReturn(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.mockito.junit.jupiter.MockitoExtension;

import com.dnd.moddo.event.application.impl.MemberExpenseReader;
import com.dnd.moddo.event.application.impl.MemberReader;
import com.dnd.moddo.event.application.impl.PaymentRequestReader;
import com.dnd.moddo.event.domain.member.ExpenseRole;
import com.dnd.moddo.event.domain.member.Member;
Expand All @@ -35,6 +36,9 @@ class PaymentRequestReaderTest {
@Mock
private MemberExpenseReader memberExpenseReader;

@Mock
private MemberReader memberReader;

@InjectMocks
private PaymentRequestReader paymentRequestReader;

Expand Down Expand Up @@ -140,6 +144,20 @@ void existsBySettlementId() {

assertThat(result).isTrue();
verify(paymentRequestRepository).existsBySettlementId(1L);
verify(memberReader, never()).existsPaidParticipant(1L);
}

@Test
@DisplayName("입금 확인 요청이 없어도 입금 완료 참여자가 있으면 수정 잠금 상태로 판단한다.")
void existsBySettlementIdWhenPaidParticipantExists() {
when(paymentRequestRepository.existsBySettlementId(1L)).thenReturn(false);
when(memberReader.existsPaidParticipant(1L)).thenReturn(true);

boolean result = paymentRequestReader.existsBySettlementId(1L);

assertThat(result).isTrue();
verify(paymentRequestRepository).existsBySettlementId(1L);
verify(memberReader).existsPaidParticipant(1L);
}

@Test
Expand Down
Loading