From dfccfa69e9fcdea7204a624ca9e4d54880c7406b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Sun, 17 May 2026 22:02:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20=EB=AA=A8=EC=9E=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/settlement.adoc | 4 ++++ .../controller/SettlementControllerTest.java | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/docs/asciidoc/settlement.adoc b/src/docs/asciidoc/settlement.adoc index 58cfd1c..9cec02e 100644 --- a/src/docs/asciidoc/settlement.adoc +++ b/src/docs/asciidoc/settlement.adoc @@ -91,6 +91,10 @@ include::{snippets}/settlement-controller-test/get-settlement/http-response.adoc include::{snippets}/settlement-controller-test/get-settlement/response-body.adoc[] +==== 응답 필드 + +include::{snippets}/settlement-controller-test/get-settlement/response-fields.adoc[] + == 모임 상단 조회 지출 내역 화면의 상단 정보를 조회할 수 있습니다. diff --git a/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java b/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java index 889f308..6a9837f 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java @@ -114,6 +114,19 @@ void getSettlement() throws Exception { .andDo(restDocs.document( pathParameters( parameterWithName("code").description("정산 코드") + ), + responseFields( + fieldWithPath("id").description("정산 ID"), + fieldWithPath("groupName").description("정산 이름"), + fieldWithPath("members").description("모임원 목록"), + fieldWithPath("members[].id").description("모임원 ID"), + fieldWithPath("members[].role").description("모임원 역할"), + fieldWithPath("members[].name").description("모임원 이름"), + fieldWithPath("members[].profile").description("프로필 이미지 URL"), + fieldWithPath("members[].userId").description("사용자 ID").optional(), + fieldWithPath("members[].isPaid").description("정산 완료 여부"), + fieldWithPath("members[].paidAt").description("정산 완료 시각").optional(), + fieldWithPath("members[].paymentRequestId").description("대기 중인 입금 확인 요청 ID").optional() ) )); } From e11aca8fab64ed4982e48be86cd199c206001075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Sun, 17 May 2026 22:08:24 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=9E=85=EA=B8=88=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EC=9A=94=EC=B2=AD=20=EC=A1=B4=EC=9E=AC=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/payment.adoc | 30 +++++++++++++++++++ .../impl/PaymentRequestReader.java | 4 +++ .../query/QueryPaymentRequestService.java | 5 ++++ .../PaymentRequestRepository.java | 7 +++++ .../PaymentRequestController.java | 10 +++++++ .../PaymentRequestExistenceResponse.java | 9 ++++++ .../PaymentRequestControllerTest.java | 24 +++++++++++++++ .../QueryPaymentRequestServiceTest.java | 12 ++++++++ .../PaymentRequestReaderTest.java | 11 +++++++ 9 files changed, 112 insertions(+) create mode 100644 src/main/java/com/dnd/moddo/event/presentation/response/PaymentRequestExistenceResponse.java diff --git a/src/docs/asciidoc/payment.adoc b/src/docs/asciidoc/payment.adoc index 7c7f919..0753e0f 100644 --- a/src/docs/asciidoc/payment.adoc +++ b/src/docs/asciidoc/payment.adoc @@ -26,6 +26,36 @@ include::{snippets}/payment-request-controller-test/get-payment-requests/http-re include::{snippets}/payment-request-controller-test/get-payment-requests/response-body.adoc[] +== 모임 입금 확인 요청 존재 여부 조회 + +모임에 생성된 입금 확인 요청이 하나라도 있는지 조회할 수 있습니다. + +=== Example + +include::{snippets}/payment-request-controller-test/exists-payment-request/curl-request.adoc[] + +=== HTTP + +==== 요청 + +include::{snippets}/payment-request-controller-test/exists-payment-request/http-request.adoc[] + +include::{snippets}/payment-request-controller-test/exists-payment-request/path-parameters.adoc[] + +==== 응답 + +include::{snippets}/payment-request-controller-test/exists-payment-request/http-response.adoc[] + +=== Body + +==== 응답 + +include::{snippets}/payment-request-controller-test/exists-payment-request/response-body.adoc[] + +==== 응답 필드 + +include::{snippets}/payment-request-controller-test/exists-payment-request/response-fields.adoc[] + == 입금 확인 요청 생성 정산 참여자가 총무에게 입금 확인 요청을 보낼 수 있습니다. diff --git a/src/main/java/com/dnd/moddo/event/application/impl/PaymentRequestReader.java b/src/main/java/com/dnd/moddo/event/application/impl/PaymentRequestReader.java index 3a6ad56..c9035e5 100644 --- a/src/main/java/com/dnd/moddo/event/application/impl/PaymentRequestReader.java +++ b/src/main/java/com/dnd/moddo/event/application/impl/PaymentRequestReader.java @@ -61,6 +61,10 @@ public PaymentRequestsResponse findByTargetUserId(Long targetUserId) { return PaymentRequestsResponse.of(responses); } + public boolean existsBySettlementId(Long settlementId) { + return paymentRequestRepository.existsBySettlementId(settlementId); + } + public Map findPendingRequestIdByMemberId(Long settlementId) { return paymentRequestRepository.findBySettlementIdAndStatus(settlementId, PaymentRequestStatus.PENDING) .stream() diff --git a/src/main/java/com/dnd/moddo/event/application/query/QueryPaymentRequestService.java b/src/main/java/com/dnd/moddo/event/application/query/QueryPaymentRequestService.java index 227cf65..550b819 100644 --- a/src/main/java/com/dnd/moddo/event/application/query/QueryPaymentRequestService.java +++ b/src/main/java/com/dnd/moddo/event/application/query/QueryPaymentRequestService.java @@ -3,6 +3,7 @@ import org.springframework.stereotype.Service; import com.dnd.moddo.event.application.impl.PaymentRequestReader; +import com.dnd.moddo.event.presentation.response.PaymentRequestExistenceResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestsResponse; import lombok.RequiredArgsConstructor; @@ -15,4 +16,8 @@ public class QueryPaymentRequestService { public PaymentRequestsResponse findByTargetUserId(Long targetUserId) { return paymentRequestReader.findByTargetUserId(targetUserId); } + + public PaymentRequestExistenceResponse existsBySettlementId(Long settlementId) { + return PaymentRequestExistenceResponse.of(paymentRequestReader.existsBySettlementId(settlementId)); + } } diff --git a/src/main/java/com/dnd/moddo/event/infrastructure/PaymentRequestRepository.java b/src/main/java/com/dnd/moddo/event/infrastructure/PaymentRequestRepository.java index 609fa7f..11c5697 100644 --- a/src/main/java/com/dnd/moddo/event/infrastructure/PaymentRequestRepository.java +++ b/src/main/java/com/dnd/moddo/event/infrastructure/PaymentRequestRepository.java @@ -33,6 +33,13 @@ default PaymentRequest getById(Long paymentRequestId) { @Query("select pr from PaymentRequest pr where pr.targetUser.id = :targetUserId") List findByTargetUserId(@Param("targetUserId") Long targetUserId); + @Query(""" + select count(pr) > 0 + from PaymentRequest pr + where pr.settlement.id = :settlementId + """) + boolean existsBySettlementId(@Param("settlementId") Long settlementId); + @Query(""" select pr from PaymentRequest pr diff --git a/src/main/java/com/dnd/moddo/event/presentation/PaymentRequestController.java b/src/main/java/com/dnd/moddo/event/presentation/PaymentRequestController.java index c18b440..3c3fe49 100644 --- a/src/main/java/com/dnd/moddo/event/presentation/PaymentRequestController.java +++ b/src/main/java/com/dnd/moddo/event/presentation/PaymentRequestController.java @@ -13,6 +13,7 @@ import com.dnd.moddo.event.application.command.CommandPaymentRequest; import com.dnd.moddo.event.application.query.QueryPaymentRequestService; import com.dnd.moddo.event.application.query.QuerySettlementService; +import com.dnd.moddo.event.presentation.response.PaymentRequestExistenceResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestsResponse; @@ -44,6 +45,15 @@ public ResponseEntity createPaymentRequest( return ResponseEntity.ok(response); } + @GetMapping("/groups/{code}/payments/exists") + public ResponseEntity existsPaymentRequest( + @PathVariable String code + ) { + Long settlementId = querySettlementService.findIdByCode(code); + PaymentRequestExistenceResponse response = queryPaymentRequestService.existsBySettlementId(settlementId); + return ResponseEntity.ok(response); + } + @PatchMapping("/payments/{paymentRequestId}/approve") public ResponseEntity approvePaymentRequest( @PathVariable Long paymentRequestId, diff --git a/src/main/java/com/dnd/moddo/event/presentation/response/PaymentRequestExistenceResponse.java b/src/main/java/com/dnd/moddo/event/presentation/response/PaymentRequestExistenceResponse.java new file mode 100644 index 0000000..f10d39c --- /dev/null +++ b/src/main/java/com/dnd/moddo/event/presentation/response/PaymentRequestExistenceResponse.java @@ -0,0 +1,9 @@ +package com.dnd.moddo.event.presentation.response; + +public record PaymentRequestExistenceResponse( + boolean exists +) { + public static PaymentRequestExistenceResponse of(boolean exists) { + return new PaymentRequestExistenceResponse(exists); + } +} diff --git a/src/test/java/com/dnd/moddo/domain/paymentRequest/controller/PaymentRequestControllerTest.java b/src/test/java/com/dnd/moddo/domain/paymentRequest/controller/PaymentRequestControllerTest.java index df39ee6..fef3927 100644 --- a/src/test/java/com/dnd/moddo/domain/paymentRequest/controller/PaymentRequestControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/paymentRequest/controller/PaymentRequestControllerTest.java @@ -19,6 +19,7 @@ import com.dnd.moddo.auth.infrastructure.security.LoginUserArgumentResolver; import com.dnd.moddo.auth.presentation.response.LoginUserInfo; import com.dnd.moddo.event.domain.paymentRequest.PaymentRequestStatus; +import com.dnd.moddo.event.presentation.response.PaymentRequestExistenceResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestsResponse; import com.dnd.moddo.global.util.RestDocsTestSupport; @@ -71,6 +72,29 @@ void getPaymentRequests() throws Exception { )); } + @Test + @DisplayName("모임에 생성된 입금 확인 요청이 있는지 확인한다.") + void existsPaymentRequest() throws Exception { + String code = "code"; + Long settlementId = 1L; + PaymentRequestExistenceResponse response = new PaymentRequestExistenceResponse(true); + + when(querySettlementService.findIdByCode(code)).thenReturn(settlementId); + when(queryPaymentRequestService.existsBySettlementId(settlementId)).thenReturn(response); + + mockMvc.perform(get("/api/v1/groups/{code}/payments/exists", code)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.exists").value(true)) + .andDo(restDocs.document( + pathParameters( + parameterWithName("code").description("정산 코드") + ), + responseFields( + fieldWithPath("exists").type(JsonFieldType.BOOLEAN).description("모임에 생성된 입금 확인 요청 존재 여부") + ) + )); + } + @Test @DisplayName("입금 확인 요청을 생성한다.") void createPaymentRequest() throws Exception { diff --git a/src/test/java/com/dnd/moddo/domain/paymentRequest/service/QueryPaymentRequestServiceTest.java b/src/test/java/com/dnd/moddo/domain/paymentRequest/service/QueryPaymentRequestServiceTest.java index 99b8161..5b2ce32 100644 --- a/src/test/java/com/dnd/moddo/domain/paymentRequest/service/QueryPaymentRequestServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/paymentRequest/service/QueryPaymentRequestServiceTest.java @@ -15,6 +15,7 @@ import com.dnd.moddo.event.application.impl.PaymentRequestReader; import com.dnd.moddo.event.application.query.QueryPaymentRequestService; +import com.dnd.moddo.event.presentation.response.PaymentRequestExistenceResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestItemResponse; import com.dnd.moddo.event.presentation.response.PaymentRequestsResponse; @@ -47,4 +48,15 @@ void findByTargetUserId() { assertThat(result).isEqualTo(expected); verify(paymentRequestReader).findByTargetUserId(1L); } + + @Test + @DisplayName("정산에 생성된 입금 확인 요청이 있는지 확인할 수 있다.") + void existsBySettlementId() { + when(paymentRequestReader.existsBySettlementId(1L)).thenReturn(true); + + PaymentRequestExistenceResponse result = queryPaymentRequestService.existsBySettlementId(1L); + + assertThat(result.exists()).isTrue(); + verify(paymentRequestReader).existsBySettlementId(1L); + } } diff --git a/src/test/java/com/dnd/moddo/domain/paymentRequest/service/implementation/PaymentRequestReaderTest.java b/src/test/java/com/dnd/moddo/domain/paymentRequest/service/implementation/PaymentRequestReaderTest.java index 53ebeab..daf136b 100644 --- a/src/test/java/com/dnd/moddo/domain/paymentRequest/service/implementation/PaymentRequestReaderTest.java +++ b/src/test/java/com/dnd/moddo/domain/paymentRequest/service/implementation/PaymentRequestReaderTest.java @@ -127,6 +127,17 @@ void findPendingRequestIdByMemberId() { assertThat(result).containsEntry(11L, 100L); } + @Test + @DisplayName("정산에 생성된 입금 확인 요청이 있는지 확인할 수 있다.") + void existsBySettlementId() { + when(paymentRequestRepository.existsBySettlementId(1L)).thenReturn(true); + + boolean result = paymentRequestReader.existsBySettlementId(1L); + + assertThat(result).isTrue(); + verify(paymentRequestRepository).existsBySettlementId(1L); + } + @Test @DisplayName("같은 멤버의 대기 중인 입금 확인 요청이 중복되면 예외가 발생한다.") void findPendingRequestIdByMemberIdFailWhenDuplicatePendingRequest() { From 3b5e50b1afb5cfc78926cea3c9142a7f5605e83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=EC=A7=80=EC=9C=A4?= Date: Sun, 17 May 2026 22:35:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=A0=95=EC=82=B0=20=EC=88=98?= =?UTF-8?q?=EB=8F=99=20=EC=99=84=EB=A3=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/settlement.adoc | 20 ++++++++ .../command/CommandSettlementService.java | 10 ++++ .../impl/SettlementCompletionProcessor.java | 15 +++++- .../application/impl/SettlementUpdater.java | 6 ++- .../event/domain/settlement/Settlement.java | 8 ++- .../infrastructure/SettlementRepository.java | 9 ++-- .../presentation/SettlementController.java | 11 +++++ .../controller/SettlementControllerTest.java | 22 +++++++++ .../service/CommandSettlementServiceTest.java | 22 +++++++++ .../SettlementCompletionProcessorTest.java | 49 +++++++++++++++++-- .../implementation/SettlementCreatorTest.java | 1 + .../implementation/SettlementUpdaterTest.java | 16 +++--- 12 files changed, 169 insertions(+), 20 deletions(-) diff --git a/src/docs/asciidoc/settlement.adoc b/src/docs/asciidoc/settlement.adoc index 9cec02e..6580378 100644 --- a/src/docs/asciidoc/settlement.adoc +++ b/src/docs/asciidoc/settlement.adoc @@ -65,6 +65,26 @@ include::{snippets}/settlement-controller-test/update-account/request-body.adoc[ include::{snippets}/settlement-controller-test/update-account/response-body.adoc[] +== 정산 수동 완료 + +총무가 정산을 수동으로 완료할 수 있습니다. + +=== Example + +include::{snippets}/settlement-controller-test/complete-settlement/curl-request.adoc[] + +=== HTTP + +==== 요청 + +include::{snippets}/settlement-controller-test/complete-settlement/http-request.adoc[] + +include::{snippets}/settlement-controller-test/complete-settlement/path-parameters.adoc[] + +==== 응답 + +include::{snippets}/settlement-controller-test/complete-settlement/http-response.adoc[] + == 모임 조회 모임 정보와 참여자 목록을 조회할 수 있습니다. diff --git a/src/main/java/com/dnd/moddo/event/application/command/CommandSettlementService.java b/src/main/java/com/dnd/moddo/event/application/command/CommandSettlementService.java index 67ea6ea..a47e5dc 100644 --- a/src/main/java/com/dnd/moddo/event/application/command/CommandSettlementService.java +++ b/src/main/java/com/dnd/moddo/event/application/command/CommandSettlementService.java @@ -5,6 +5,7 @@ import com.dnd.moddo.common.cache.CacheEvictor; import com.dnd.moddo.event.application.impl.MemberCreator; +import com.dnd.moddo.event.application.impl.SettlementCompletionProcessor; import com.dnd.moddo.event.application.impl.SettlementCreator; import com.dnd.moddo.event.application.impl.SettlementReader; import com.dnd.moddo.event.application.impl.SettlementUpdater; @@ -28,6 +29,7 @@ public class CommandSettlementService { private final SettlementValidator settlementValidator; private final SettlementReader settlementReader; private final MemberCreator memberCreator; + private final SettlementCompletionProcessor settlementCompletionProcessor; public SettlementSaveResponse createSettlement(SettlementRequest request, Long userId) { Settlement settlement = settlementCreator.createSettlement(request, userId); @@ -43,4 +45,12 @@ public SettlementResponse updateAccount(SettlementAccountRequest request, Long u cacheEvictor.evictSettlementHeader(settlementId); return SettlementResponse.of(settlement); } + + public void completeSettlement(Long userId, Long settlementId) { + Settlement settlement = settlementReader.read(settlementId); + settlementValidator.checkSettlementAuthor(settlement, userId); + settlementCompletionProcessor.complete(settlementId); + cacheEvictor.evictSettlementHeader(settlementId); + cacheEvictor.evictSettlementListsBySettlement(settlementId); + } } diff --git a/src/main/java/com/dnd/moddo/event/application/impl/SettlementCompletionProcessor.java b/src/main/java/com/dnd/moddo/event/application/impl/SettlementCompletionProcessor.java index ccf59d7..a26994d 100644 --- a/src/main/java/com/dnd/moddo/event/application/impl/SettlementCompletionProcessor.java +++ b/src/main/java/com/dnd/moddo/event/application/impl/SettlementCompletionProcessor.java @@ -1,8 +1,11 @@ package com.dnd.moddo.event.application.impl; +import java.time.LocalDateTime; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.dnd.moddo.event.domain.settlement.Settlement; import com.dnd.moddo.outbox.application.command.CommandOutboxEventService; import com.dnd.moddo.outbox.domain.event.type.AggregateType; import com.dnd.moddo.outbox.domain.event.type.OutboxEventType; @@ -13,6 +16,7 @@ @RequiredArgsConstructor public class SettlementCompletionProcessor { private final MemberReader memberReader; + private final SettlementReader settlementReader; private final SettlementUpdater settlementUpdater; private final CommandOutboxEventService commandOutboxEventService; @@ -22,8 +26,15 @@ public boolean completeIfAllPaid(Long settlementId) { return false; } - boolean completed = settlementUpdater.complete(settlementId); - if (completed) { + return complete(settlementId); + } + + @Transactional + public boolean complete(Long settlementId) { + Settlement settlement = settlementReader.read(settlementId); + LocalDateTime completedAt = LocalDateTime.now(); + boolean completed = settlementUpdater.complete(settlementId, completedAt); + if (completed && settlement.isCompletedWithinDeadline(completedAt)) { commandOutboxEventService.create(OutboxEventType.SETTLEMENT_COMPLETED, AggregateType.SETTLEMENT, settlementId); } diff --git a/src/main/java/com/dnd/moddo/event/application/impl/SettlementUpdater.java b/src/main/java/com/dnd/moddo/event/application/impl/SettlementUpdater.java index f3e061d..c19582d 100644 --- a/src/main/java/com/dnd/moddo/event/application/impl/SettlementUpdater.java +++ b/src/main/java/com/dnd/moddo/event/application/impl/SettlementUpdater.java @@ -1,5 +1,7 @@ package com.dnd.moddo.event.application.impl; +import java.time.LocalDateTime; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,7 +23,7 @@ public Settlement updateAccount(SettlementAccountRequest request, Long settlemen return settlement; } - public boolean complete(Long settlementId) { - return settlementRepository.markCompletedIfNotCompleted(settlementId) == 1; + public boolean complete(Long settlementId, LocalDateTime completedAt) { + return settlementRepository.markCompletedIfNotCompleted(settlementId, completedAt) == 1; } } diff --git a/src/main/java/com/dnd/moddo/event/domain/settlement/Settlement.java b/src/main/java/com/dnd/moddo/event/domain/settlement/Settlement.java index bbe3613..2ebaa38 100644 --- a/src/main/java/com/dnd/moddo/event/domain/settlement/Settlement.java +++ b/src/main/java/com/dnd/moddo/event/domain/settlement/Settlement.java @@ -54,12 +54,12 @@ public Settlement(String name, Long writer, LocalDateTime createdAt, String bank, String accountNumber, String code, LocalDateTime deadline) { this.name = name; this.writer = writer; - this.createdAt = createdAt; + this.createdAt = createdAt != null ? createdAt : LocalDateTime.now(); this.expiredAt = LocalDateTime.now().plusMonths(1); this.bank = bank; this.accountNumber = accountNumber; this.code = code; - this.deadline = deadline; + this.deadline = deadline != null ? deadline : this.createdAt.plusDays(1); } public void updateAccount(SettlementAccountRequest request) { @@ -79,4 +79,8 @@ public boolean isWriter(Long userId) { public void complete() { this.completedAt = LocalDateTime.now(); } + + public boolean isCompletedWithinDeadline(LocalDateTime completedAt) { + return deadline != null && !completedAt.isAfter(deadline); + } } diff --git a/src/main/java/com/dnd/moddo/event/infrastructure/SettlementRepository.java b/src/main/java/com/dnd/moddo/event/infrastructure/SettlementRepository.java index 7172a2a..d759b19 100644 --- a/src/main/java/com/dnd/moddo/event/infrastructure/SettlementRepository.java +++ b/src/main/java/com/dnd/moddo/event/infrastructure/SettlementRepository.java @@ -66,11 +66,14 @@ SELECT COUNT(s) @Modifying @Query(""" update Settlement s - set s.completedAt = CURRENT_TIMESTAMP + set s.completedAt = :completedAt where s.id = :settlementId and s.completedAt is null - """) - int markCompletedIfNotCompleted(@Param("settlementId") Long settlementId); + """) + int markCompletedIfNotCompleted( + @Param("settlementId") Long settlementId, + @Param("completedAt") LocalDateTime completedAt + ); default Long getIdByCode(String code) { return findIdByCode(code).orElseThrow(() -> new GroupNotFoundException(code)); diff --git a/src/main/java/com/dnd/moddo/event/presentation/SettlementController.java b/src/main/java/com/dnd/moddo/event/presentation/SettlementController.java index bc55866..c2b9a6d 100644 --- a/src/main/java/com/dnd/moddo/event/presentation/SettlementController.java +++ b/src/main/java/com/dnd/moddo/event/presentation/SettlementController.java @@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -62,6 +63,16 @@ public ResponseEntity updateAccount( return ResponseEntity.ok(response); } + @PatchMapping("/{code}/complete") + public ResponseEntity completeSettlement( + @PathVariable("code") String code, + @LoginUser LoginUserInfo loginUser + ) { + Long settlementId = querySettlementService.findIdByCode(code); + commandSettlementService.completeSettlement(loginUser.userId(), settlementId); + return ResponseEntity.ok().build(); + } + @GetMapping("/{code}") public ResponseEntity getSettlement( HttpServletRequest request, diff --git a/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java b/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java index 6a9837f..76bb4f6 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/controller/SettlementControllerTest.java @@ -86,6 +86,28 @@ void updateAccount() throws Exception { )); } + @Test + @DisplayName("정산을 수동 완료한다.") + void givenExistingSettlement_thenCompleteSettlement() throws Exception { + // given + given(loginUserArgumentResolver.supportsParameter(any())) + .willReturn(true); + + given(loginUserArgumentResolver.resolveArgument(any(), any(), any(), any())) + .willReturn(new LoginUserInfo(1L, "USER")); + given(querySettlementService.findIdByCode("code")).willReturn(100L); + willDoNothing().given(commandSettlementService).completeSettlement(1L, 100L); + + // when & then + mockMvc.perform(patch("/api/v1/groups/{code}/complete", "code")) + .andExpect(status().isOk()) + .andDo(restDocs.document( + pathParameters( + parameterWithName("code").description("정산 코드") + ) + )); + } + @Test @DisplayName("모임을 성공적으로 조회한다.") void getSettlement() throws Exception { diff --git a/src/test/java/com/dnd/moddo/domain/settlement/service/CommandSettlementServiceTest.java b/src/test/java/com/dnd/moddo/domain/settlement/service/CommandSettlementServiceTest.java index 297193f..e547f61 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/service/CommandSettlementServiceTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/service/CommandSettlementServiceTest.java @@ -19,6 +19,7 @@ import com.dnd.moddo.event.application.command.CommandSettlementService; import com.dnd.moddo.event.application.impl.MemberCreator; import com.dnd.moddo.event.application.impl.MemberReader; +import com.dnd.moddo.event.application.impl.SettlementCompletionProcessor; import com.dnd.moddo.event.application.impl.SettlementCreator; import com.dnd.moddo.event.application.impl.SettlementReader; import com.dnd.moddo.event.application.impl.SettlementUpdater; @@ -54,6 +55,8 @@ class CommandSettlementServiceTest { private MemberReader memberReader; @Mock private CacheEvictor cacheEvictor; + @Mock + private SettlementCompletionProcessor settlementCompletionProcessor; @InjectMocks private CommandSettlementService commandSettlementService; @@ -126,4 +129,23 @@ void updateGroupAccount() { verify(cacheEvictor).evictSettlementHeader(settlement.getId()); } + @Test + @DisplayName("그룹을 수동 완료할 수 있다.") + void givenExistingSettlement_thenCompleteSettlement() { + // given + when(settlementReader.read(anyLong())).thenReturn(settlement); + doNothing().when(settlementValidator).checkSettlementAuthor(any(Settlement.class), anyLong()); + when(settlementCompletionProcessor.complete(1L)).thenReturn(true); + + // when + commandSettlementService.completeSettlement(1L, 1L); + + // then + verify(settlementReader, times(1)).read(1L); + verify(settlementValidator, times(1)).checkSettlementAuthor(settlement, 1L); + verify(settlementCompletionProcessor, times(1)).complete(1L); + verify(cacheEvictor, times(1)).evictSettlementHeader(1L); + verify(cacheEvictor, times(1)).evictSettlementListsBySettlement(1L); + } + } diff --git a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCompletionProcessorTest.java b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCompletionProcessorTest.java index 2000a4a..21d28fd 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCompletionProcessorTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCompletionProcessorTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.time.LocalDateTime; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,7 +14,9 @@ import com.dnd.moddo.event.application.impl.MemberReader; import com.dnd.moddo.event.application.impl.SettlementCompletionProcessor; +import com.dnd.moddo.event.application.impl.SettlementReader; import com.dnd.moddo.event.application.impl.SettlementUpdater; +import com.dnd.moddo.event.domain.settlement.Settlement; import com.dnd.moddo.outbox.application.command.CommandOutboxEventService; import com.dnd.moddo.outbox.domain.event.type.AggregateType; import com.dnd.moddo.outbox.domain.event.type.OutboxEventType; @@ -23,6 +27,9 @@ class SettlementCompletionProcessorTest { @Mock private MemberReader memberReader; + @Mock + private SettlementReader settlementReader; + @Mock private SettlementUpdater settlementUpdater; @@ -40,33 +47,65 @@ void doesNotCompleteWhenUnpaidMemberExists() { boolean result = settlementCompletionProcessor.completeIfAllPaid(1L); assertThat(result).isFalse(); - verify(settlementUpdater, never()).complete(anyLong()); + verify(settlementUpdater, never()).complete(anyLong(), any(LocalDateTime.class)); verify(commandOutboxEventService, never()).create(any(), any(), anyLong()); } @Test @DisplayName("정산이 방금 완료되면 아웃박스 이벤트를 생성한다.") void createsOutboxEventWhenSettlementCompleted() { + Settlement settlement = createSettlement(LocalDateTime.now().plusDays(1)); + when(memberReader.existsUnpaidMember(1L)).thenReturn(false); - when(settlementUpdater.complete(1L)).thenReturn(true); + when(settlementReader.read(1L)).thenReturn(settlement); + when(settlementUpdater.complete(eq(1L), any(LocalDateTime.class))).thenReturn(true); boolean result = settlementCompletionProcessor.completeIfAllPaid(1L); assertThat(result).isTrue(); - verify(settlementUpdater).complete(1L); + verify(settlementUpdater).complete(eq(1L), any(LocalDateTime.class)); verify(commandOutboxEventService).create(OutboxEventType.SETTLEMENT_COMPLETED, AggregateType.SETTLEMENT, 1L); } @Test @DisplayName("이미 완료된 정산이면 아웃박스 이벤트를 생성하지 않는다.") void doesNotCreateOutboxEventWhenSettlementAlreadyCompleted() { + Settlement settlement = createSettlement(LocalDateTime.now().plusDays(1)); + when(memberReader.existsUnpaidMember(1L)).thenReturn(false); - when(settlementUpdater.complete(1L)).thenReturn(false); + when(settlementReader.read(1L)).thenReturn(settlement); + when(settlementUpdater.complete(eq(1L), any(LocalDateTime.class))).thenReturn(false); boolean result = settlementCompletionProcessor.completeIfAllPaid(1L); assertThat(result).isFalse(); - verify(settlementUpdater).complete(1L); + verify(settlementUpdater).complete(eq(1L), any(LocalDateTime.class)); verify(commandOutboxEventService, never()).create(any(), any(), anyLong()); } + + @Test + @DisplayName("마감일 이후 완료된 정산이면 아웃박스 이벤트를 생성하지 않는다.") + void givenCompletedAfterDeadline_thenDoesNotCreateOutboxEvent() { + Settlement settlement = createSettlement(LocalDateTime.now().minusSeconds(1)); + + when(memberReader.existsUnpaidMember(1L)).thenReturn(false); + when(settlementReader.read(1L)).thenReturn(settlement); + when(settlementUpdater.complete(eq(1L), any(LocalDateTime.class))).thenReturn(true); + + boolean result = settlementCompletionProcessor.completeIfAllPaid(1L); + + assertThat(result).isTrue(); + verify(settlementUpdater).complete(eq(1L), any(LocalDateTime.class)); + verify(commandOutboxEventService, never()).create(any(), any(), anyLong()); + } + + private Settlement createSettlement(LocalDateTime deadline) { + return Settlement.builder() + .name("group") + .writer(1L) + .createdAt(LocalDateTime.now().minusDays(1)) + .code("code") + .deadline(deadline) + .build(); + } } diff --git a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCreatorTest.java b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCreatorTest.java index 9eb930f..c2f00f4 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCreatorTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementCreatorTest.java @@ -85,6 +85,7 @@ void createSettlementSuccess() { // then assertThat(response).isNotNull(); assertThat(response.getName()).isEqualTo(request.name()); + assertThat(response.getDeadline()).isNotNull(); verify(userRepository, times(1)).getById(userId); verify(settlementRepository, times(1)).save(any(Settlement.class)); diff --git a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementUpdaterTest.java b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementUpdaterTest.java index 8f09743..a0f4c56 100644 --- a/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementUpdaterTest.java +++ b/src/test/java/com/dnd/moddo/domain/settlement/service/implementation/SettlementUpdaterTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; +import java.time.LocalDateTime; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -59,24 +61,26 @@ void updateAccountNotFoundGroup() { @Test void completeSuccess() { Long settlementId = 1L; - when(settlementRepository.markCompletedIfNotCompleted(settlementId)).thenReturn(1); + LocalDateTime completedAt = LocalDateTime.now(); + when(settlementRepository.markCompletedIfNotCompleted(settlementId, completedAt)).thenReturn(1); - boolean result = settlementUpdater.complete(settlementId); + boolean result = settlementUpdater.complete(settlementId, completedAt); assertThat(result).isTrue(); - verify(settlementRepository).markCompletedIfNotCompleted(settlementId); + verify(settlementRepository).markCompletedIfNotCompleted(settlementId, completedAt); } @DisplayName("이미 완료된 정산이면 다시 완료 처리하지 않는다.") @Test void completeAlreadyCompletedSettlement() { Long settlementId = 1L; - when(settlementRepository.markCompletedIfNotCompleted(settlementId)).thenReturn(0); + LocalDateTime completedAt = LocalDateTime.now(); + when(settlementRepository.markCompletedIfNotCompleted(settlementId, completedAt)).thenReturn(0); - boolean result = settlementUpdater.complete(settlementId); + boolean result = settlementUpdater.complete(settlementId, completedAt); assertThat(result).isFalse(); - verify(settlementRepository).markCompletedIfNotCompleted(settlementId); + verify(settlementRepository).markCompletedIfNotCompleted(settlementId, completedAt); } }