Skip to content

[FEAT] 환불 기한 1년으로 제한#346

Closed
unifolio0 wants to merge 1 commit intodevelopfrom
feat/#345
Closed

[FEAT] 환불 기한 1년으로 제한#346
unifolio0 wants to merge 1 commit intodevelopfrom
feat/#345

Conversation

@unifolio0
Copy link
Copy Markdown
Contributor

@unifolio0 unifolio0 commented Mar 4, 2026

closed #345

작업 내용

진짜 나는 카와이한 오상훈이다. 나보다 잘생긴놈 나오라고해.

스크린샷

참고 사항

@unifolio0 unifolio0 self-assigned this Mar 4, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 토큰 환불 가능 기간을 구매일로부터 1년으로 제한
    • 환불 기한 만료 상태 추가
  • 버그 수정

    • 기한이 만료된 토큰에 대한 환불 요청 차단

🐰 환불 기한 1년 제한 기능 추가

워크스루

토큰 구매에 1년 환불 만료 기한을 도입합니다. 도메인 모델, 상태 열거형, 응답 매핑, 서비스 검증 로직, 그리고 통합 및 단위 테스트를 수정하여 구매 1년 후 환불 불가능하도록 제한합니다.

변경 사항

코호트 / 파일 요약
도메인 모델
src/main/java/com/samhap/kokomen/token/domain/TokenPurchase.java
REFUND_EXPIRY_YEARS 상수 추가 및 isRefundExpired() 메서드 신규 구현. isRefundable()을 수정하여 환불 만료 여부를 검증하도록 변경.
상태 열거형
src/main/java/com/samhap/kokomen/token/domain/TokenPurchaseState.java
새로운 상태 상수 REFUND_EXPIRED("환불 기한 만료") 추가.
응답 매핑
src/main/java/com/samhap/kokomen/token/dto/TokenPurchaseResponse.java
resolveDisplayState() 헬퍼 메서드 추가하여 REFUNDABLE 상태에서 만료된 경우 REFUND_EXPIRED 메시지 반환하도록 처리.
서비스 검증
src/main/java/com/samhap/kokomen/token/service/TokenFacadeService.java
refundTokens() 메서드에 만료 확인 조건 추가하여 소유권 검증 후 환불 만료 시 예외 발생.
통합 테스트
src/test/java/com/samhap/kokomen/token/controller/TokenControllerTest.java
JdbcTemplate 의존성 주입 및 구매일로부터_1년_경과한_토큰_환불_실패() 테스트 메서드 추가. 생성 시점을 1년 이상 전으로 조작하여 환불 실패 검증.
단위 테스트
src/test/java/com/samhap/kokomen/token/domain/TokenPurchaseTest.java
TokenPurchaseTest 신규 테스트 클래스 추가하여 1년 이내, 1년 초과, 정확히 1년 시점의 isRefundable()isRefundExpired() 검증.

예상 코드 리뷰 노력

🎯 3 (Moderate) | ⏱️ ~20 minutes

관련 가능성 있는 PR

추천 검토자

  • nak-honest
  • kargowild

시 🐰

어느덧 일 년이 흘렀으니
환불 창구는 닫혔네요 🔐
제때 신청하는 지혜,
그것이 진정한 보상이라 생각하는 우리의 시스템 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 환불 기한 1년 제한이라는 PR의 핵심 변경사항을 명확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed 설명에서 #345 이슈를 참조하고 있으며, 변경사항과 관련이 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 1년 환불 기한 제한 요구사항을 충족합니다. TokenPurchase에 REFUND_EXPIRY_YEARS 상수 추가, isRefundExpired() 메서드 구현, TokenPurchaseState에 REFUND_EXPIRED 상태 추가, 환불 시 기한 확인 로직 추가, 테스트 케이스 작성 완료.
Out of Scope Changes check ✅ Passed 모든 변경사항이 1년 환불 기한 제한이라는 링크된 이슈 목표와 직접 관련이 있으며 범위를 벗어나지 않습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#345

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 토큰 구매에 대한 환불 정책을 강화하기 위해 환불 기한을 1년으로 제한하는 기능을 구현합니다. 기존 환불 가능 여부 로직에 기한 만료 조건을 추가하고, 사용자에게 표시되는 환불 상태를 세분화하며, 환불 요청 처리 시 기한 만료 여부를 엄격하게 검증하도록 시스템을 개선했습니다. 이는 비즈니스 요구사항을 반영하고 데이터 일관성을 유지하는 데 기여합니다.

Highlights

  • 환불 기한 로직 추가: 토큰 구매에 대해 1년의 환불 기한을 설정하는 로직이 TokenPurchase 도메인에 추가되었습니다.
  • 환불 상태 확장: TokenPurchaseState에 '환불 기한 만료' 상태가 새로 정의되었습니다.
  • 환불 API 유효성 검사 강화: 토큰 환불 요청 시 환불 기한 만료 여부를 확인하여, 기한이 지난 경우 환불을 거부하도록 서비스 로직이 업데이트되었습니다.
  • 테스트 코드 보강: 환불 기한 로직의 정확성을 검증하기 위한 단위 및 통합 테스트가 추가되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/main/java/com/samhap/kokomen/token/domain/TokenPurchase.java
    • 환불 기한 상수를 추가했습니다.
    • isRefundable() 메서드에 환불 기한 만료 여부 확인 로직을 추가했습니다.
    • isRefundExpired() 메서드를 새로 추가하여 구매일로부터 1년이 경과했는지 확인합니다.
  • src/main/java/com/samhap/kokomen/token/domain/TokenPurchaseState.java
    • REFUND_EXPIRED 열거형 값을 추가했습니다.
  • src/main/java/com/samhap/kokomen/token/dto/TokenPurchaseResponse.java
    • TokenPurchaseState를 임포트했습니다.
    • from() 메서드에서 resolveDisplayState()를 사용하여 환불 상태 메시지를 결정하도록 변경했습니다.
    • resolveDisplayState() 정적 메서드를 추가하여 환불 가능 상태이면서 기한이 만료된 경우 '환불 기한 만료' 메시지를 반환합니다.
  • src/main/java/com/samhap/kokomen/token/service/TokenFacadeService.java
    • refundTokens() 메서드에 환불 기한 만료 시 BadRequestException을 발생시키는 로직을 추가했습니다.
  • src/test/java/com/samhap/kokomen/token/controller/TokenControllerTest.java
    • JdbcTemplate 의존성을 추가했습니다.
    • 구매일로부터 1년이 경과한 토큰의 환불 실패를 테스트하는 통합 테스트 케이스를 추가했습니다.
  • src/test/java/com/samhap/kokomen/token/domain/TokenPurchaseTest.java
    • TokenPurchase 도메인의 환불 기한 로직을 검증하는 단위 테스트 파일이 새로 추가되었습니다.
Activity
  • 이 Pull Request는 unifolio0에 의해 생성되었습니다.
  • #345 이슈를 해결합니다.
  • 현재까지 추가적인 리뷰 활동이나 코멘트는 없습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


일 년의 시간, 환불의 문 닫히네. 신중한 구매.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

안녕하세요. 환불 기한을 1년으로 제한하는 기능 추가에 대한 코드 변경사항을 검토했습니다. 전반적으로 기능 구현이 잘 이루어졌고, 도메인 로직 변경에 따른 단위 테스트와 통합 테스트가 모두 추가되어 코드의 안정성을 높인 점이 좋습니다. 한 가지 개선 제안 사항으로, 도메인 모델에서 시스템 시간에 직접 의존하는 부분을 분리하여 테스트 용이성을 높이는 방안을 검토해보시면 좋을 것 같습니다. 자세한 내용은 관련 코드에 의견을 남겼습니다.

Comment on lines 180 to 186
public boolean isRefundable() {
return state == TokenPurchaseState.REFUNDABLE && purchaseCount.equals(remainingCount);
return state == TokenPurchaseState.REFUNDABLE && purchaseCount.equals(remainingCount) && !isRefundExpired();
}

public boolean isRefundExpired() {
return getCreatedAt().plusYears(REFUND_EXPIRY_YEARS).isBefore(LocalDateTime.now());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

도메인 모델 내에서 LocalDateTime.now()를 직접 호출하면 시스템 시계에 대한 숨은 의존성이 생겨 테스트하기 어려운 코드가 될 수 있습니다. 시간과 관련된 로직은 외부에서 현재 시간을 주입받는 형태로 변경하는 것을 고려해보세요. 이렇게 하면 테스트 시 시간을 제어하기 용이해져 코드의 안정성과 예측 가능성을 높일 수 있습니다.

예를 들어, isRefundableisRefundExpired 메서드가 LocalDateTime을 인자로 받도록 수정하고, 서비스 레이어에서 LocalDateTime.now()를 전달하는 방식입니다. 이 변경은 isNotRefundable 등 관련 메서드들의 시그니처 변경을 수반합니다. 이렇게 수정하면 TokenPurchaseTest에서 리플렉션을 사용하여 createdAt을 설정하는 대신, 테스트 시간을 명시적으로 전달하여 더 깔끔한 테스트를 작성할 수 있습니다.

Suggested change
public boolean isRefundable() {
return state == TokenPurchaseState.REFUNDABLE && purchaseCount.equals(remainingCount);
return state == TokenPurchaseState.REFUNDABLE && purchaseCount.equals(remainingCount) && !isRefundExpired();
}
public boolean isRefundExpired() {
return getCreatedAt().plusYears(REFUND_EXPIRY_YEARS).isBefore(LocalDateTime.now());
}
public boolean isRefundable(java.time.LocalDateTime now) {
return state == TokenPurchaseState.REFUNDABLE && purchaseCount.equals(remainingCount) && !isRefundExpired(now);
}
public boolean isRefundExpired(java.time.LocalDateTime now) {
return getCreatedAt().plusYears(REFUND_EXPIRY_YEARS).isBefore(now);
}

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2026

Test Results

 49 files   49 suites   1m 29s ⏱️
274 tests 273 ✅ 1 💤 0 ❌
276 runs  275 ✅ 1 💤 0 ❌

Results for commit e2a560c.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/java/com/samhap/kokomen/token/controller/TokenControllerTest.java`:
- Around line 57-58: Replace the fully-qualified type declaration on the
jdbcTemplate field in TokenControllerTest with an import: add import for
org.springframework.jdbc.core.JdbcTemplate at the top and change the field to
"private JdbcTemplate jdbcTemplate;" (keeping the `@Autowired` annotation and
field name intact) so the class uses the simple type name for improved
readability.
- Around line 633-667: The test 구매일로부터_1년_경과한_토큰_환불_실패 currently only asserts
status 400 which could be triggered by other validation errors; update
TokenControllerTest (the test method 구매일로부터_1년_경과한_토큰_환불_실패) to also assert the
failure reason by checking the response body for the specific error code or
message your controller returns for expired refunds (e.g. an "expired" error
code or message like "TOKEN_REFUND_EXPIRED" or "refund period expired") using
mockMvc.perform(...).andExpect(...). For example, after the existing
.andExpect(status().isBadRequest()) add an assertion that inspects the JSON
error field (jsonPath("$.code") or jsonPath("$.message")) or the response string
to ensure the error matches the expiration case so the test cannot pass for
unrelated BadRequest causes.

In `@src/test/java/com/samhap/kokomen/token/domain/TokenPurchaseTest.java`:
- Around line 43-50: The test method 구매일로부터_정확히_1년이면_환불_가능하다 has a mismatch
between its name and setup: it sets createdAt to
LocalDateTime.now().minusYears(1).plusMinutes(1) which is not exactly one year;
either change the createdAt value to LocalDateTime.now().minusYears(1) (adjust
any helper call setCreatedAt(tokenPurchase, ...)) or rename the test to reflect
“약 1년”/“1년보다 조금 지난 경우”; update the TokenPurchaseFixtureBuilder usage and the
setCreatedAt call accordingly so the test name and setup are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb6175bc-bc1b-4167-9747-7d5c6db25b2a

📥 Commits

Reviewing files that changed from the base of the PR and between 800fa17 and e2a560c.

📒 Files selected for processing (6)
  • src/main/java/com/samhap/kokomen/token/domain/TokenPurchase.java
  • src/main/java/com/samhap/kokomen/token/domain/TokenPurchaseState.java
  • src/main/java/com/samhap/kokomen/token/dto/TokenPurchaseResponse.java
  • src/main/java/com/samhap/kokomen/token/service/TokenFacadeService.java
  • src/test/java/com/samhap/kokomen/token/controller/TokenControllerTest.java
  • src/test/java/com/samhap/kokomen/token/domain/TokenPurchaseTest.java

Comment on lines +57 to +58
@Autowired
private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

타입 선언은 import를 사용하면 가독성이 더 좋습니다.

필드에서 FQCN 직접 사용 대신 import로 정리하면 테스트 클래스 읽기가 수월해집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/samhap/kokomen/token/controller/TokenControllerTest.java`
around lines 57 - 58, Replace the fully-qualified type declaration on the
jdbcTemplate field in TokenControllerTest with an import: add import for
org.springframework.jdbc.core.JdbcTemplate at the top and change the field to
"private JdbcTemplate jdbcTemplate;" (keeping the `@Autowired` annotation and
field name intact) so the class uses the simple type name for improved
readability.

Comment on lines +633 to +667
@Test
void 구매일로부터_1년_경과한_토큰_환불_실패() throws Exception {
// given
Member member = memberRepository.save(MemberFixtureBuilder.builder().build());
tokenService.createTokensForNewMember(member.getId());

TokenPurchase tokenPurchase = tokenPurchaseRepository.save(
TokenPurchaseFixtureBuilder.builder()
.memberId(member.getId())
.count(10)
.remainingCount(10)
.state(TokenPurchaseState.REFUNDABLE)
.build()
);

// created_at을 1년 전으로 변경 (native SQL로 @CreatedDate 필드 직접 수정)
jdbcTemplate.update(
"UPDATE token_purchase SET created_at = ? WHERE id = ?",
java.time.LocalDateTime.now().minusYears(1).minusDays(1),
tokenPurchase.getId()
);

MockHttpSession session = new MockHttpSession();
session.setAttribute("MEMBER_ID", member.getId());

TokenRefundRequest request = new TokenRefundRequest(RefundReasonCode.CHANGE_OF_MIND, null);

// when & then
mockMvc.perform(patch("/api/v1/token-purchases/{tokenPurchaseId}/refund", tokenPurchase.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.header("Cookie", "JSESSIONID=" + session.getId())
.session(session))
.andExpect(status().isBadRequest());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

실패 원인까지 검증해 테스트 의도를 고정해 주세요.

현재는 400만 확인해서, 만료 외 다른 BadRequest로도 통과할 수 있습니다. 만료 메시지(또는 에러 코드)까지 함께 검증하는 편이 안전합니다.

🔧 보강 예시
         mockMvc.perform(patch("/api/v1/token-purchases/{tokenPurchaseId}/refund", tokenPurchase.getId())
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(request))
                         .header("Cookie", "JSESSIONID=" + session.getId())
                         .session(session))
-                .andExpect(status().isBadRequest());
+                .andExpect(status().isBadRequest())
+                .andExpect(result -> assertThat(result.getResponse().getContentAsString())
+                        .contains("구매일로부터 1년이 경과하여 환불이 불가합니다."));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/samhap/kokomen/token/controller/TokenControllerTest.java`
around lines 633 - 667, The test 구매일로부터_1년_경과한_토큰_환불_실패 currently only asserts
status 400 which could be triggered by other validation errors; update
TokenControllerTest (the test method 구매일로부터_1년_경과한_토큰_환불_실패) to also assert the
failure reason by checking the response body for the specific error code or
message your controller returns for expired refunds (e.g. an "expired" error
code or message like "TOKEN_REFUND_EXPIRED" or "refund period expired") using
mockMvc.perform(...).andExpect(...). For example, after the existing
.andExpect(status().isBadRequest()) add an assertion that inspects the JSON
error field (jsonPath("$.code") or jsonPath("$.message")) or the response string
to ensure the error matches the expiration case so the test cannot pass for
unrelated BadRequest causes.

Comment on lines +43 to +50
void 구매일로부터_정확히_1년이면_환불_가능하다() {
// given
TokenPurchase tokenPurchase = TokenPurchaseFixtureBuilder.builder()
.state(TokenPurchaseState.REFUNDABLE)
.count(10)
.remainingCount(10)
.build();
setCreatedAt(tokenPurchase, LocalDateTime.now().minusYears(1).plusMinutes(1));
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

테스트 이름과 설정 시간이 불일치합니다.

구매일로부터_정확히_1년이면_환불_가능하다인데, 실제 데이터는 minusYears(1).plusMinutes(1)로 “정확히 1년”이 아닙니다. 테스트명을 바꾸거나 시간을 정확히 1년으로 맞춰 주세요.

🔧 수정 예시
-    void 구매일로부터_정확히_1년이면_환불_가능하다() {
+    void 구매일로부터_1년_미만이면_환불_가능하다() {
...
-        setCreatedAt(tokenPurchase, LocalDateTime.now().minusYears(1).plusMinutes(1));
+        setCreatedAt(tokenPurchase, LocalDateTime.now().minusYears(1).plusMinutes(1));

또는

-        setCreatedAt(tokenPurchase, LocalDateTime.now().minusYears(1).plusMinutes(1));
+        setCreatedAt(tokenPurchase, LocalDateTime.now().minusYears(1));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/samhap/kokomen/token/domain/TokenPurchaseTest.java` around
lines 43 - 50, The test method 구매일로부터_정확히_1년이면_환불_가능하다 has a mismatch between
its name and setup: it sets createdAt to
LocalDateTime.now().minusYears(1).plusMinutes(1) which is not exactly one year;
either change the createdAt value to LocalDateTime.now().minusYears(1) (adjust
any helper call setCreatedAt(tokenPurchase, ...)) or rename the test to reflect
“약 1년”/“1년보다 조금 지난 경우”; update the TokenPurchaseFixtureBuilder usage and the
setCreatedAt call accordingly so the test name and setup are consistent.

@unifolio0 unifolio0 closed this Mar 11, 2026
@unifolio0 unifolio0 deleted the feat/#345 branch March 11, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 환불 기한 1년으로 제한

1 participant