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이므로 프론트 배포와 동시에 나가야 합니다.


✨ 기타 설명 / 질문

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