fix(receipt): 월별 구매 조회가 환불·검증실패 영수증까지 세던 문제 - #480
Merged
Conversation
`get_user_receipts_by_month`가 status를 전혀 보지 않아, 스토어 검증에 실패한 영수증(INVALID)도 "이번 달에 구매함"으로 집계됐다. 시즌패스 보유 판정 (`/api/admin/user-receipts/*` 5개 엔드포인트)이 이 함수를 쓰므로, 그런 영수증 하나가 그 달의 재구매를 통째로 막는다. 실제 사고: 8/07 결제가 미확인 결제로 자동 환불됐는데, 8/14 배포(#477)로 막혀 있던 클라이언트 재시도가 풀리면서 8/17 에 그 결제의 영수증이 뒤늦게 생성됐고 구글 재검증에서 이미 환불된 건이라 INVALID 가 됐다. 집계 기준이 purchased_at 이 아니라 created_at 이라 8월 건으로 잡혔고, 웹샵에서 시즌패스를 다시 살 수 없었다. 운영 DB 확인: 해당 계정의 8월 집계가 수정 전 1건(INVALID) → 수정 후 0건. 구매 제한(get_purchase_count)은 애초에 INVALID 를 제외해 0건이었다. 즉 두 함수의 기준이 달라 "제한엔 안 걸리는데 보유 판정엔 걸리는" 상태였다. 집계 대상을 INIT/VALIDATION_REQUEST/VALID 로 좁힌다(get_purchase_count 와 동일한 상태 집합). 유료 상품 기준 12개월간 새로 제외되는 건은 6행(4명)뿐이라 지출 집계(non-pass-*)에 미치는 영향은 무시할 수준임을 운영 DB 로 확인했다. 범위 밖(후속 과제): - created_at 버킷팅. 월말 결제가 다음 달에 영수증화되면 VALID 인 정상 결제도 같은 차단이 난다. #477 로 밀린 재시도가 도달하기 시작해 노출도가 오른 상태다. - 지급 후 환불된 건은 여전히 VALID 다. REFUNDED_BY_ADMIN/BUYER 를 기록하는 경로가 없어 이 필터로 안 걸러진다. - 인게임 경로는 get_purchase_history 를 쓰며 이미 같은 상태 필터가 있다. 즉 이 PR 은 웹샵 차단만 푼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ipdae
force-pushed
the
yang/hotfix-monthly-receipt-status
branch
from
August 18, 2026 12:49
791e2f6 to
9e12ebc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
증상
시즌패스를 산 유저가 게임에서도 웹샵에서도 재구매가 막힙니다. 구매 가능 수량은
3/3(=3개 남음)으로 정상 표시되는데도 구매가 실패합니다.원인
Receipt.get_user_receipts_by_month가status를 전혀 보지 않습니다.그래서 검증 실패(
INVALID)나 환불된 영수증도 "이번 달에 구매함"으로 집계됩니다. 이 함수는 시즌패스 보유 판정에 쓰이는 5개 엔드포인트가 공유합니다.반면 구매 제한 계산(
get_purchase_count)은 이미INIT/VALIDATION_REQUEST/VALID만 셉니다. 두 함수의 기준이 달라서 "제한엔 안 걸리는데 보유 판정엔 걸리는" 상태가 생깁니다.실제 사고 경로
집계 기준이
purchased_at이 아니라 **created_at**이라, 8/07 구매건이 8/17에 생성되며 8월 건으로 잡혔습니다.#477이 만든 버그는 아니지만, 그동안 도달하지 못하던 재시도를 풀어주면서 이 선재 결함이 드러났습니다.
수정
집계 대상을
get_purchase_count와 동일하게 맞춥니다.검증 (운영 DB, 읽기 전용)
문제 계정의 8월 집계입니다.
구매 제한(
get_purchase_count) 쪽은 애초에 0건이었습니다.리뷰 반영
코드 리뷰에서 나온 지적을 반영했다.
non-pass-*지출 집계 영향 측정 — 리뷰는 제외 대상이 6,816건(PURCHASE_LIMIT_EXCEED 등)이라 지출이 과소집계될 수 있다고 봤으나, 그 대부분은 무료·마일리지 영수증이고non-pass-*는only_paid_products=True(가격>0)라 애초에 세지 않는다. 운영 DB 실측 결과 유료 상품 기준 12개월간 새로 제외되는 건은 6행(4명), 그중 non-pass는 5행이다. 범위 분리는 하지 않는다.REFUNDED_BY_*는 아무 데서도 기록되지 않으므로 이 PR이 실제로 거르는 건 "검증 실패"뿐이다. "환불까지 걸러진다"는 표현을 뺐다.get_purchase_count와의 관계 정확화 — 상태 집합만 일치시켰고 시간 기준(purchased_atKST 일/주 vscreated_atUTC 월)은 여전히 다르다는 점을 주석에 명시했다.inspect.getsource문자열 검사를 버리고, 세션을 가로채filter()에 들어간 SQLAlchemy 표현식을 컴파일해 검증한다. 조건식을 만들어두고 filter에 안 넣는 회귀를 실제로 잡는 것까지 확인했다(해당 줄 제거 시 2개 실패).범위 밖 (후속 과제)
created_at버킷팅 — 월말 결제가 다음 달에 영수증화되면VALID인 정상 결제도 같은 차단이 난다. #477로 밀려 있던 재시도가 도달하기 시작해 노출도가 오른 상태다.REFUNDED_BY_ADMIN/REFUNDED_BY_BUYER를 기록하는 경로가 없어 그런 영수증은VALID로 남고 이 필터로 안 걸러진다.get_purchase_history를 쓰며 이미 같은 상태 필터가 있다. 이 PR은 웹샵 차단만 푼다.영향 범위
INIT/VALIDATION_REQUEST(검증 진행 중)를 포함해 중복 구매를 막는 보수적 판정은 유지합니다tests/shared/test_receipt_month_status.py)배포
스키마 변경 없음. 다만 main 에 바우처(#476)가 머지돼 있고 그 마이그레이션이 운영에 미적용이라, 배포 시
alembic upgrade head를 함께 태우는 것을 권한다(테이블 2개 CREATE + merge revision).운영 확인:
WORKER_VOUCHER_GRANT_ENABLED미설정, 시크릿에voucher-grant-enabled없음 → 바우처 워커는 기본값(False)로 early return 하므로 미적용 상태에서도 에러가 나지 않는다.🤖 Generated with Claude Code