Skip to content

[FEAT] 프롬프트 구매 환불 정책 완성 — 7일 KST 기준 정정, 환불 가능 플래그, 열람 후 수동 환불 워크플로 #533

Description

@minij02

✨ 기능 설명

환불 정책이 확정됨에 따라, 기존 환불 기능(#485, #497, #518)과 정책 사이의 차이를 메우고 열람 후 수동 환불(최장 3개월) 워크플로를 추가합니다.

확정 정책

A. 자동 환불 — 열람 전, 7일 이내

  • 마이페이지 > 구매한 프롬프트에서 환불 신청
  • 프롬프트 열람 X + 구매 후 7일 이내인 경우에만 환불 신청 버튼 활성화
  • 7일 기준: 시간대 고려 X. 23일에 구매했다면 30일까지 신청 가능 (첫날 제외)
  • 결제 화면에 "디지털콘텐츠 특성상 열람(제공 개시) 후에는 단순 변심 환불이 불가합니다" 문구 + 동의 체크박스

B. 수동 환불 — 열람 후, 담당자 확인, 최장 3개월
단순 변심은 불가. 아래 사유에 한해 담당자가 수동 확인 후 환불:

  • 프롬프트 내용이 과하게 부실한 경우
    • 본문이 비어 있거나, 의미 있는 지시문이라 볼 수 없는 내용
    • 본문 분량·구성이 상세페이지 안내 수준에 현저히 미달
    • 명시된 AI 모델에서 실행해도 상세페이지 예시와 같은 범주의 결과물을 얻을 수 없음
  • 유료 프롬프트 내용이 작성자가 직접 작성한 것이 아니라 외부에서 가져온 경우
    • 예: 지원한다고 표시한 모델에서 작동 안 함, 설명된 기능과 무관한 내용

현재 develop 상태

항목 상태 위치
Payple 결제취소 연동 (PCD_PAYCANCEL_FLAG=Y) src/settlements/utils/payple-refund.ts
Refund 모델, Purchase.downloaded_at prisma/schema.prisma
사용자 환불 API 2종 src/refunds/routes/refund.route.ts
Payment/Settlement → Refunded 전이 refund.service.ts:135-144
다운로드 목록에 purchase_id / is_refunded #518
7일 계산이 정책과 불일치 refund.service.ts:10,50 — 168시간 절대값
환불 신청 버튼 활성화 근거 없음 목록 API에 refundable 부재
수동 환불 신청/승인 워크플로 Refund에 상태값 없음, 관리자 API 없음
결제 시 환불정책 동의 기록

✨ 개발 목록

1. 7일 기준을 KST 날짜 기준으로 정정

현재 refund.service.ts:50Date.now() - created_at >= 168시간으로 판정합니다. 23일 15시 구매 시 30일 15시에 마감되어, "30일까지 가능"이라는 정책과 어긋납니다.

  • REFUND_WINDOW_MS 상수 제거, KST 날짜 기준 마감 계산으로 교체
    • 마감 시각 = 구매일(KST) + 8일 00:00 KST (= D+7일 24:00까지)
    • 23일 구매 → 31일 00:00 KST 직전까지, 즉 30일 23:59:59까지 신청 가능
  • remaining_seconds를 새 마감 기준으로 재계산
  • 응답에 refund_deadline (ISO8601) 추가 — FE가 "N일 남음"을 직접 계산할 수 있도록
  • 경계값 자체 검증 추가 (23일 00:00 / 30일 23:59:59 / 31일 00:00 KST)

2. 환불 가능 여부 판정 로직 공용화 + 목록 노출

정책 판정이 refund.service.ts에만 있어, 목록 화면에서 버튼 활성화를 판단할 방법이 없습니다. FE가 항목마다 refund-eligibility를 호출하면 N+1이 됩니다.

  • checkEligibility의 판정 로직을 순수 함수로 분리 (src/refunds/utils/refund-policy.ts)
    • 입력: { user_id, created_at, downloaded_at, is_free, payment.status, refund }
    • 출력: { eligible, reason, refund_deadline, remaining_seconds }
    • 단건 API와 목록 API가 이 함수 하나를 공유 — 정책이 두 곳에 복제되지 않도록
  • refund.service.ts가 이 함수를 쓰도록 리팩터링 (동작 변화 없음)
  • PromptDownloadRepository.getDownloadedPromptsByUser select 보강
    • created_at, downloaded_at, is_free, payment: { select: { status: true } } 추가
  • DownloadedPromptResponseDTO에 필드 추가
    • refundable: boolean — 자동 환불(열람 전 7일 이내) 신청 버튼 활성화 여부
    • refund_deadline: string | null — 자동 환불 마감 시각
    • manual_refund_available: boolean — 열람 후 3개월 이내 → 수동 환불 신청 가능
  • prompt.download.route.ts Swagger 응답 스키마 갱신

3. Refund 모델에 상태 추가

  • schema.prisma 수정
    enum RefundStatus {
      REQUESTED   // 사용자 신청, 담당자 검토 대기
      APPROVED    // 승인됨 (Payple 취소 실패 시 여기서 정지 → 수동 송금)
      REJECTED    // 거절됨
      COMPLETED   // 환불 완료 (Payple 취소 성공)
    }
    
    model Refund {
      ...
      status         RefundStatus @default(COMPLETED)
      request_reason String?      @db.VarChar(500)  // 사용자 신청 사유 (수동 환불)
      reject_reason  String?      @db.VarChar(500)  // 관리자 거절 사유
      reviewed_by    Int?                            // 처리한 관리자 user_id
      reviewed_at    DateTime?
      payple_fail_code String?    @db.VarChar(40)   // 취소 실패 시 PCD_PAY_CODE
      requested_at   DateTime     @default(now())
      @@index([status])
    }
  • 마이그레이션 생성 — 기존 자동 환불 레코드는 COMPLETED 기본값으로 백필
  • refunded_at은 실제 환불 완료 시점 의미로 유지 (REQUESTED 단계에서는 미확정)

4. 수동 환불 신청 API (사용자)

  • POST /api/prompts/purchases/:purchaseId/refund-request
    • 조건: 본인 구매 / 유료 / payment.status === 'Succeed' / 환불 이력 없음 / 열람함(downloaded_at !== null) / 구매 후 3개월 이내
    • body: { reason: string } (필수, 10자 이상 500자 이하)
    • Refund 레코드를 status: REQUESTED, initiator: 'USER'로 생성
    • 열람 전 + 7일 이내라면 신청이 아니라 기존 즉시 환불(POST .../refund)로 안내 (400)
  • 관리자에게 알림 발송 여부 검토 (NotificationType 확장 필요 시 별도 논의)

5. 관리자 환불 관리 API

admin-seller.route.tspending 목록 → 상세 → 승인 → 거절 구조를 그대로 따릅니다. 신규 라우터 src/refunds/routes/admin-refund.route.ts, /api/admin/refunds에 마운트.

  • GET /api/admin/refunds/pendingstatus: REQUESTED 목록 (페이지네이션)
  • GET /api/admin/refunds/:refundId — 상세 (구매자, 프롬프트 본문, 상세페이지 설명, 신청 사유, 열람 시점)
    • 담당자가 "본문이 부실한지"를 판단해야 하므로 프롬프트 본문과 상세페이지 설명을 함께 반환
  • PATCH /api/admin/refunds/:refundId/approve — 승인 → Payple 취소 호출
  • PATCH /api/admin/refunds/:refundId/reject — 거절, body { reason: string }
  • GET /api/admin/refunds — 전체 이력 (status 필터)
  • 전 엔드포인트 authenticateJwt + isAdmin
  • Swagger 문서화

6. Payple 취소 실패 시 처리 (3개월 경과 건)

카드사 취소 가능 기간을 넘긴 건은 Payple 취소 API가 거절합니다. 승인 자체를 롤백하면 시스템상 영영 환불 불가로 남으므로, 승인 상태로 멈추고 오프라인 처리로 넘깁니다.

  • 승인 처리 흐름
    1. status: APPROVED + reviewed_by/reviewed_at 먼저 기록
    2. Payple 취소 호출
    3. 성공 → COMPLETED + refunded_at + Payment/Settlement Refunded 전이
    4. 실패 → APPROVED 유지 + payple_fail_code 기록, 응답은 200이되 payple_cancel_failed: true 로 관리자에게 명시
  • APPROVED 상태(= 취소 실패 대기)를 관리자 목록에서 별도로 필터링 가능하게
  • 수동 송금 완료 후 COMPLETED로 전이시키는 엔드포인트: PATCH /:refundId/complete-manual

7. 결제 화면 환불정책 동의 기록

  • PurchaseRequestDTOrefund_policy_agreed: boolean 추가 — true가 아니면 400 RefundPolicyNotAgreed
  • Purchase.refund_policy_agreed_at DateTime? 컬럼 추가
  • PCD_USER_DEFINE1agreed_at 포함 → purchase.complete에서 Purchase 생성 시 기록
  • Swagger에 문구 원문 명시:

    디지털콘텐츠 특성상 열람(제공 개시) 후에는 단순 변심 환불이 불가합니다

8. 검증

  • 7일 경계 계산 자체 검증 (src/refunds/utils/refund-policy.test.ts 또는 self-check)
  • pnpm build / pnpm tsc --noEmit
  • 기존 자동 환불 경로 회귀 확인 (열람 전 7일 이내 → 즉시 환불이 그대로 동작)

✨ API 변경 요약 (프론트 동기화)

Method Path 비고
GET /api/prompts/downloads 응답에 refundable, refund_deadline, manual_refund_available 추가
GET /api/prompts/purchases/{id}/refund-eligibility 응답에 refund_deadline 추가, 7일 판정 기준 변경
POST /api/prompts/purchases/{id}/refund 기존 유지 (열람 전 7일 이내 즉시 환불)
POST /api/prompts/purchases/{id}/refund-request 신규 — 열람 후 수동 환불 신청
POST /api/prompts/purchases/request refund_policy_agreed: true 필수화 (breaking)
GET /api/admin/refunds/pending 신규
GET /api/admin/refunds/{refundId} 신규
PATCH /api/admin/refunds/{refundId}/approve 신규
PATCH /api/admin/refunds/{refundId}/reject 신규
PATCH /api/admin/refunds/{refundId}/complete-manual 신규

⚠️ refund_policy_agreed 필수화는 breaking change이므로 프론트 배포와 동시에 나가야 합니다.


✨ 기타 설명 / 질문

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions