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 @@ -61,4 +61,16 @@ public PaymentRequestsResponse findByTargetUserId(Long targetUserId) {
return PaymentRequestsResponse.of(responses);
}

public Map<Long, Long> findPendingRequestIdByMemberId(Long settlementId) {
return paymentRequestRepository.findBySettlementIdAndStatus(settlementId, PaymentRequestStatus.PENDING)
.stream()
.collect(Collectors.toMap(
PaymentRequest::getRequestMemberId,
PaymentRequest::getId,
(first, duplicate) -> {
throw new IllegalStateException("중복된 PENDING 입금 확인 요청이 존재합니다.");
}
));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import org.springframework.stereotype.Component;

import com.dnd.moddo.auth.model.exception.UserPermissionException;
import com.dnd.moddo.event.domain.member.Member;
import com.dnd.moddo.event.domain.paymentRequest.PaymentRequest;
import com.dnd.moddo.event.domain.paymentRequest.PaymentRequestStatus;
import com.dnd.moddo.event.domain.paymentRequest.exception.DuplicatePendingPaymentRequestException;
import com.dnd.moddo.event.domain.paymentRequest.exception.ManagerPaymentRequestNotAllowedException;
import com.dnd.moddo.event.domain.paymentRequest.exception.PaymentRequestAlreadyApprovedException;
import com.dnd.moddo.event.domain.paymentRequest.exception.PaymentRequestNotPendingException;
import com.dnd.moddo.event.domain.paymentRequest.exception.PaymentRequestUnauthorizedException;
import com.dnd.moddo.event.infrastructure.PaymentRequestRepository;

import lombok.RequiredArgsConstructor;
Expand All @@ -26,8 +26,8 @@ public void validateCreateRequest(Long settlementId, Member requestMember) {
}

public void validateProcessRequest(PaymentRequest paymentRequest, Long userId) {
validateManagerPermission(paymentRequest, userId);
validatePendingStatus(paymentRequest);
validateTargetUser(paymentRequest, userId);
}
Comment thread
sudhdkso marked this conversation as resolved.

private void validateDuplicateRequest(Long settlementId, Long requestMemberId) {
Expand Down Expand Up @@ -66,9 +66,9 @@ private void validatePendingStatus(PaymentRequest paymentRequest) {
}
}

private void validateTargetUser(PaymentRequest paymentRequest, Long userId) {
if (!paymentRequest.getTargetUser().getId().equals(userId)) {
throw new PaymentRequestUnauthorizedException(paymentRequest.getId(), userId);
private void validateManagerPermission(PaymentRequest paymentRequest, Long userId) {
if (!paymentRequest.getSettlement().isWriter(userId)) {
throw new UserPermissionException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import java.time.Duration;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.dnd.moddo.common.cache.CacheExecutor;
import com.dnd.moddo.common.cache.CacheKeys;
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.member.Member;
Expand All @@ -29,14 +31,16 @@ public class QuerySettlementService {
private static final Duration SETTLEMENT_LIST_CACHE_TTL = Duration.ofMinutes(5);

private final SettlementReader settlementReader;
private final PaymentRequestReader paymentRequestReader;
private final SettlementValidator settlementValidator;
private final CacheExecutor cacheExecutor;

public SettlementDetailResponse findOne(Long settlementId, Long userId) {
Settlement settlement = settlementReader.read(settlementId);
settlementValidator.checkSettlementAuthor(settlement, userId);
List<Member> members = settlementReader.findBySettlement(settlementId);
return SettlementDetailResponse.of(settlement, members);
Map<Long, Long> paymentRequestIdByMemberId = paymentRequestReader.findPendingRequestIdByMemberId(settlementId);
return SettlementDetailResponse.of(settlement, members, paymentRequestIdByMemberId);
}

public SettlementHeaderResponse findBySettlementHeader(Long settlementId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ default PaymentRequest getById(Long paymentRequestId) {

@Query("select pr from PaymentRequest pr where pr.targetUser.id = :targetUserId")
List<PaymentRequest> findByTargetUserId(@Param("targetUserId") Long targetUserId);

@Query("""
select pr
from PaymentRequest pr
where pr.settlement.id = :settlementId
and pr.status = :status
""")
List<PaymentRequest> findBySettlementIdAndStatus(
@Param("settlementId") Long settlementId,
@Param("status") PaymentRequestStatus status
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.moddo.event.presentation.response;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.dnd.moddo.event.domain.member.Member;
Expand All @@ -9,11 +10,19 @@
public record SettlementDetailResponse(
Long id,
String groupName,
List<MemberResponse> members
List<SettlementMemberResponse> members
) {
public static SettlementDetailResponse of(Settlement settlement, List<Member> members) {
List<MemberResponse> memberResponses = members.stream()
.map(MemberResponse::of)
return of(settlement, members, Map.of());
}

public static SettlementDetailResponse of(
Settlement settlement,
List<Member> members,
Map<Long, Long> paymentRequestIdByMemberId
) {
List<SettlementMemberResponse> memberResponses = members.stream()
.map(member -> SettlementMemberResponse.of(member, paymentRequestIdByMemberId.get(member.getId())))
.collect(Collectors.toList());
return new SettlementDetailResponse(
settlement.getId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.dnd.moddo.event.presentation.response;

import java.time.LocalDateTime;

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

import lombok.Builder;

@Builder
public record SettlementMemberResponse(
Long id,
ExpenseRole role,
String name,
String profile,
Long userId,
Boolean isPaid,
LocalDateTime paidAt,
Long paymentRequestId
) {

public static SettlementMemberResponse of(Member member, Long paymentRequestId) {
return SettlementMemberResponse.builder()
.id(member.getId())
.name(member.getName())
.role(member.getRole())
.userId(member.getUserId())
.isPaid(member.isPaid())
.paidAt(member.getPaidAt())
.paymentRequestId(paymentRequestId)
.profile(member.getProfileUrl())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private void appendSettlementCompletedTasks(OutboxEvent outboxEvent) {
for (Member member : memberReader.findAssignedMembersBySettlementId(outboxEvent.getAggregateId())) {
Long targetUserId = member.getUserId();
eventTasks.add(EventTask.pending(outboxEvent, EventTaskType.REWARD_GRANT, targetUserId));
eventTasks.add(EventTask.pending(outboxEvent, EventTaskType.NOTIFICATION_SEND, targetUserId));
// eventTasks.add(EventTask.pending(outboxEvent, EventTaskType.NOTIFICATION_SEND, targetUserId));
}

if (!eventTasks.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,12 @@ void appendTasksForSettlementCompleted() {

ArgumentCaptor<List<EventTask>> captor = ArgumentCaptor.forClass(List.class);
verify(eventTaskCreator).createAll(captor.capture());
assertThat(captor.getValue()).hasSize(4);
assertThat(captor.getValue()).hasSize(2);
assertThat(captor.getValue())
.extracting(EventTask::getTaskType, EventTask::getTargetUserId)
.containsExactly(
tuple(EventTaskType.REWARD_GRANT, 20L),
tuple(EventTaskType.NOTIFICATION_SEND, 20L),
tuple(EventTaskType.REWARD_GRANT, 30L),
tuple(EventTaskType.NOTIFICATION_SEND, 30L)
tuple(EventTaskType.REWARD_GRANT, 30L)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,66 @@ void findByTargetUserId() {
assertThat(second.totalAmount()).isEqualTo(5000L);
}

@Test
@DisplayName("정산의 대기 중인 입금 확인 요청 ID를 멤버 ID 기준으로 조회할 수 있다.")
void findPendingRequestIdByMemberId() {
Settlement settlement = mock(Settlement.class);
Member member = Member.builder()
.name("김반숙")
.profileId(1)
.settlement(settlement)
.role(ExpenseRole.PARTICIPANT)
.build();
PaymentRequest paymentRequest = PaymentRequest.builder()
.settlement(settlement)
.requestMember(member)
.targetUser(mock(com.dnd.moddo.user.domain.User.class))
.build();

setField(member, "id", 11L);
setField(paymentRequest, "id", 100L);

when(paymentRequestRepository.findBySettlementIdAndStatus(1L, PaymentRequestStatus.PENDING))
.thenReturn(List.of(paymentRequest));

java.util.Map<Long, Long> result = paymentRequestReader.findPendingRequestIdByMemberId(1L);

assertThat(result).containsEntry(11L, 100L);
}
Comment on lines +125 to +128
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

맵 결과 크기 단언을 추가해 테스트 정확도를 높여주세요.

containsEntry만으로는 불필요한 추가 엔트리가 있어도 테스트가 통과합니다. 기대 개수를 함께 고정해 회귀를 더 잘 잡는 게 좋습니다.

제안 수정안
 java.util.Map<Long, Long> result = paymentRequestReader.findPendingRequestIdByMemberId(1L);

+assertThat(result).hasSize(1);
 assertThat(result).containsEntry(11L, 100L);
📝 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
java.util.Map<Long, Long> result = paymentRequestReader.findPendingRequestIdByMemberId(1L);
assertThat(result).containsEntry(11L, 100L);
}
java.util.Map<Long, Long> result = paymentRequestReader.findPendingRequestIdByMemberId(1L);
assertThat(result).hasSize(1);
assertThat(result).containsEntry(11L, 100L);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/dnd/moddo/domain/paymentRequest/service/implementation/PaymentRequestReaderTest.java`
around lines 125 - 128, The test in PaymentRequestReaderTest should also assert
the expected map size to prevent extra unexpected entries; after calling
paymentRequestReader.findPendingRequestIdByMemberId(1L) and before or alongside
assertThat(result).containsEntry(11L, 100L), add an assertion that result has
the exact expected size (e.g., assertThat(result).hasSize(<expectedCount>))
referencing the local variable result and the tested method
findPendingRequestIdByMemberId to lock the expected number of entries.


@Test
@DisplayName("같은 멤버의 대기 중인 입금 확인 요청이 중복되면 예외가 발생한다.")
void findPendingRequestIdByMemberIdFailWhenDuplicatePendingRequest() {
Settlement settlement = mock(Settlement.class);
Member member = Member.builder()
.name("김반숙")
.profileId(1)
.settlement(settlement)
.role(ExpenseRole.PARTICIPANT)
.build();
PaymentRequest first = PaymentRequest.builder()
.settlement(settlement)
.requestMember(member)
.targetUser(mock(com.dnd.moddo.user.domain.User.class))
.build();
PaymentRequest second = PaymentRequest.builder()
.settlement(settlement)
.requestMember(member)
.targetUser(mock(com.dnd.moddo.user.domain.User.class))
.build();

setField(member, "id", 11L);
setField(first, "id", 100L);
setField(second, "id", 101L);

when(paymentRequestRepository.findBySettlementIdAndStatus(1L, PaymentRequestStatus.PENDING))
.thenReturn(List.of(first, second));

assertThatThrownBy(() -> paymentRequestReader.findPendingRequestIdByMemberId(1L))
.isInstanceOf(IllegalStateException.class)
.hasMessage("중복된 PENDING 입금 확인 요청이 존재합니다.");
}

private void setField(Object target, String fieldName, Object value) {
try {
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.dnd.moddo.auth.model.exception.UserPermissionException;
import com.dnd.moddo.event.application.impl.PaymentRequestValidator;
import com.dnd.moddo.event.domain.member.Member;
import com.dnd.moddo.event.domain.paymentRequest.PaymentRequest;
Expand All @@ -18,9 +19,8 @@
import com.dnd.moddo.event.domain.paymentRequest.exception.ManagerPaymentRequestNotAllowedException;
import com.dnd.moddo.event.domain.paymentRequest.exception.PaymentRequestAlreadyApprovedException;
import com.dnd.moddo.event.domain.paymentRequest.exception.PaymentRequestNotPendingException;
import com.dnd.moddo.event.domain.paymentRequest.exception.PaymentRequestUnauthorizedException;
import com.dnd.moddo.event.domain.settlement.Settlement;
import com.dnd.moddo.event.infrastructure.PaymentRequestRepository;
import com.dnd.moddo.user.domain.User;

@ExtendWith(MockitoExtension.class)
class PaymentRequestValidatorTest {
Expand Down Expand Up @@ -71,25 +71,41 @@ void validateCreateRequestFailWhenAlreadyApproved() {
}

@Test
@DisplayName("처리 대상 유저가 아니면 승인 또는 거절할 수 없다.")
void validateProcessRequestFailWhenUnauthorized() {
@DisplayName("총무가 아니면 승인 또는 거절할 수 없다.")
void validateProcessRequestFailWhenNotManager() {
PaymentRequest paymentRequest = mock(PaymentRequest.class);
User targetUser = mock(User.class);
Settlement settlement = mock(Settlement.class);

when(paymentRequest.getStatus()).thenReturn(PaymentRequestStatus.PENDING);
when(paymentRequest.getTargetUser()).thenReturn(targetUser);
when(paymentRequest.getId()).thenReturn(1L);
when(targetUser.getId()).thenReturn(100L);
when(paymentRequest.getSettlement()).thenReturn(settlement);
when(settlement.isWriter(200L)).thenReturn(false);

assertThatThrownBy(() -> paymentRequestValidator.validateProcessRequest(paymentRequest, 200L))
.isInstanceOf(PaymentRequestUnauthorizedException.class);
.isInstanceOf(UserPermissionException.class);
verify(paymentRequest, never()).getStatus();
}

@Test
@DisplayName("총무이면 승인 또는 거절할 수 있다.")
void validateProcessRequestSuccessWhenManager() {
PaymentRequest paymentRequest = mock(PaymentRequest.class);
Settlement settlement = mock(Settlement.class);

when(paymentRequest.getStatus()).thenReturn(PaymentRequestStatus.PENDING);
when(paymentRequest.getSettlement()).thenReturn(settlement);
when(settlement.isWriter(100L)).thenReturn(true);

assertThatCode(() -> paymentRequestValidator.validateProcessRequest(paymentRequest, 100L))
.doesNotThrowAnyException();
}

@Test
@DisplayName("대기 상태가 아니면 승인 또는 거절할 수 없다.")
void validateProcessRequestFailWhenNotPending() {
PaymentRequest paymentRequest = mock(PaymentRequest.class);
Settlement settlement = mock(Settlement.class);

when(paymentRequest.getSettlement()).thenReturn(settlement);
when(settlement.isWriter(100L)).thenReturn(true);
when(paymentRequest.getStatus()).thenReturn(PaymentRequestStatus.APPROVED);
when(paymentRequest.getId()).thenReturn(1L);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.dnd.moddo.event.presentation.response.SettlementDetailResponse;
import com.dnd.moddo.event.presentation.response.SettlementHeaderResponse;
import com.dnd.moddo.event.presentation.response.SettlementListResponse;
import com.dnd.moddo.event.presentation.response.SettlementMemberResponse;
import com.dnd.moddo.event.presentation.response.SettlementResponse;
import com.dnd.moddo.event.presentation.response.SettlementSaveResponse;
import com.dnd.moddo.event.presentation.response.SettlementShareResponse;
Expand Down Expand Up @@ -90,10 +91,11 @@ void updateAccount() throws Exception {
void getSettlement() throws Exception {
// given
SettlementDetailResponse response = new SettlementDetailResponse(1L, "모또 모임", List.of(
new MemberResponse(1L, MANAGER, "김모또", "https://moddo-s3.s3.amazonaws.com/profile/MODDO.png",
new SettlementMemberResponse(1L, MANAGER, "김모또", "https://moddo-s3.s3.amazonaws.com/profile/MODDO.png",
1L,
true,
LocalDateTime.now())
LocalDateTime.now(),
null)
));

given(loginUserArgumentResolver.supportsParameter(any()))
Expand All @@ -108,6 +110,7 @@ void getSettlement() throws Exception {
// when & then
mockMvc.perform(get("/api/v1/groups/{code}", "code"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.members[0].paymentRequestId").value(Matchers.nullValue()))
.andDo(restDocs.document(
pathParameters(
parameterWithName("code").description("정산 코드")
Expand Down
Loading
Loading