Skip to content

feat(voucher): 회수/리컨사일 — 환불 방어 (PLD-1470/1471) - #474

Closed
ipdae wants to merge 2 commits into
yang/pld-1469-voucher-grant-triggerfrom
yang/pld-1470-voucher-reconcile
Closed

feat(voucher): 회수/리컨사일 — 환불 방어 (PLD-1470/1471)#474
ipdae wants to merge 2 commits into
yang/pld-1469-voucher-grant-triggerfrom
yang/pld-1470-voucher-reconcile

Conversation

@ipdae

@ipdae ipdae commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

요약

환불 감지 → 포탈 revoke 호출을 아웃박스(voucher_grant_outbox)를 단일 조율점으로 처리. grant 트리거(#472) 위에 스택.

두 신호원 → REVOKE_PENDING

  • google buyer 환불: track_google_refund가 void 감지 시 order_id→receipt→enqueue_revoke (google 환불은 receipt.status를 갱신하지 않으므로 이 훅이 유일 신호원). 알림 흐름과 독립·best-effort.
  • admin 환불/무효(status=REFUNDED_*/INVALID): reconcile의 status 기반 enroll.

reconcile beat(*/5분)

  • (A) status enroll: 환불/무효 receipt의 미회수 아웃박스 → REVOKE_PENDING
  • (B) revoke dispatch: REVOKE_PENDING → 포탈 revoke → REVOKED. 행잠금(skip_locked)·행별 격리·재시도

아웃박스가 단일 조율점인 이유

REVOKE_PENDING/REVOKED 행은 grant enroll(notin_)·dispatch(PENDING만)가 모두 스킵 → 환불이 grant보다 먼저 도착해도 발급이 선점적으로 차단된다(아웃박스를 REVOKE_PENDING로 생성).

안전

  • revoke 유실 = 미회수(환불 NCG 잔존)이므로 4xx도 드롭하지 않고 REVOKE_PENDING 유지 + 경보
  • 포탈 revoke 자체가 iapUuid 멱등(미개봉만 회수, 개봉건 skippedOpened+경보)

테스트 14건

_post_revoke 응답분류, enqueue_revoke_for_receipt 상태전이(생성/전이/no-op), 게이팅.

ipdae and others added 2 commits August 4, 2026 00:16
환불 감지 → 포탈 revoke를 아웃박스(voucher_grant_outbox)를 단일 조율점으로 처리.

- voucher_reconcile_task(beat */5분):
  (A) status 기반 enroll: 환불/무효(REFUNDED_*/INVALID) receipt의 미회수 아웃박스 → REVOKE_PENDING (admin 환불 등)
  (B) revoke dispatch: REVOKE_PENDING → 포탈 revoke → REVOKED. 행잠금(skip_locked)·행별 격리·재시도
- track_google_refund 훅: void 감지 시 order_id→receipt→enqueue_revoke (google buyer 환불은
  receipt.status 미갱신이라 이 훅이 유일 신호원). 알림 흐름과 독립·best-effort
- 아웃박스가 단일 조율점: REVOKE_PENDING/REVOKED는 grant enroll(notin_)·dispatch(PENDING)가 스킵
  → 환불이 grant보다 먼저 도착해도 발급 선점 차단
- revoke는 유실=미회수(환불 NCG 잔존)이므로 4xx도 드롭 않고 REVOKE_PENDING 유지+경보
- 테스트 14건(revoke 응답분류, enqueue 상태전이, 게이팅)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…470/1471)

🔴#1 훅 배치 UNIQUE 충돌 통째 롤백 → google 환불 회수 영구 유실:
  enqueue_revoke_for_receipt 생성 경로를 begin_nested(SAVEPOINT)로 행별 격리,
  충돌(grant 선점) 시 재조회 후 REVOKE_PENDING 전이

🔴#2 크래시창에 발급 후 FAILED 찍힌 건이 enroll scope 밖 → 회수 누락:
  status 기반 enroll scope에 FAILED 포함(revoke는 미발급 receipt엔 멱등 no-op이라 무해)

🟡:
- transient(5xx/인증/레이트리밋) 스톨은 failed에 안 잡혀 무알림 → attempts>=5 REVOKE_PENDING
  백로그 집계해 경보(회수 유실 진행중 가시화)
- dispatch order_by(attempts, receipt_id) → 영구 4xx 고-attempts 행이 신규 회수 starve 방지
- enqueue_revoke_by_order_id에 google 스토어 필터 + 다중매치 전건 큐잉(크로스-스토어 오회수 방지)
- reconcile 엔진을 grant와 공유(프로세스당 커넥션 풀 이중생성 방지)
- Apple buyer 환불 미커버 known-gap 명시(ASSN 처리기 도입 시 연결)

테스트 17건(+FAILED 전이, order_id 다중매치/무매치)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ipdae

ipdae commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

복권식(lottery) 리팩터로 대체됨 → #475 로 통합. (이 PR의 리뷰·엔벨로프는 #475에 반영됨)

@ipdae ipdae closed this Aug 5, 2026
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.

1 participant