What I Learn Today

Start Date : 2022/02/07 ~

Learn/Company

[TIL #18] 구글 환불 API 구현 + Flask 프레임워크 공부

HannaDev 2022. 3. 9. 00:05

구글 환불 API (revoke, refund) 업무 1차 완료 !

어제 우당탕탕 구글 환불 API 를 구현하고 테스트했었는데요.
생각보다 간단하게 반나절만에 (?) 끝나버렸습니다.

이미 구현되어 있는 구글 결제 API 라우터 함수를 참고하니 금방 끝났네요!
테스트를 어떻게 해볼까가 문제였지 구글 API 사용은 매우 간단했습니다.

다만... 문제는 오늘 더이상 업무가 없었다는 거....? ㅎㅎ;;;;
신입이지만 오전 10시 이후로 할 일이 없어서 곤란했습니다.

자체적으로 일감을 만들어내볼까 했는데 바로 컷트당해서 (*ˊᵕˋ*)ノ
오늘은 'Flask 기반의 파이썬 웹 프로그래밍' 책 보면서 프레임워크를 공부해보았습니다.

하루종일 독서만 한 8시간 가까이 한 것 같네요...!


▶ 전체적인 스케쥴

  • 이동 (집 -> 회사) - 1.5h
    • [독서] Focus - 시간 관리 전략
  • 자리 세팅 - 10분
  • 구글 환불 API : refund 추가 구현 및 테스트 - 0.5h
  • 개발 스크럼 회의 - 0.5h

 

<오늘의 주의사항>

1. Jira Ticket 은 자유롭게 생성해도 되지만 '완료' 상태로 마음대로 이동시키면 안된다.
   => '완료' 상태는 서비스가 나갔을 때를 의미한다.
   => 개발을 1차적으로 완료한 경우에는 '대기' 상태에 두어라.

2. 스크럼 회의에서 '업무 상태' 를 짧게 브리핑할 때는 모두가 이해할 수 있게 배경 설명을 덧붙여라.
   => 스크럼 회의는 상사에게 보고하는 시간이 아니라 팀 목표와 업무 진행 상황을 '공유'하는 시간이다.
   => 이런 의미에서 자주 실수하는 부분이 배경 설명 없이 결과만 말하는 것이다.
   => 모든 팀원이 이해할 수 있도록 한 depth 상위에서 설명했으면 한다. (=사전 지식 없다고 생각하기)

 

  • [Flask 공부] 'Flask 기반의 파이썬 웹 프로그래밍' - 1.5h
  • 점심 ⇒ 동기 분들과 대화 - 1h
  • [Flask 공부] 'Flask 기반의 파이썬 웹 프로그래밍' - 3.5h
  • 커피 마시면서 휴식 (잠 깨기) - 0.5h
  • 구글 환불 API 관련 추가 업무 확인 - 1h

 

<구글 환불 API 업무 다음 단계>

1. _revoke(...) 형태로 private 함수 작성하기
    => 현재 Command 함수 형태로 구현한 구글 환불 기능은 동작 확인 완료.
    => 이를 실제 업무에서 사용할 수 있게 ~~~ .py 파일에 private 함수로 작성하라.

2. 실패 시, Exception 던지기
    => exceptions.py 을 참고하여 Exception 을 정의하고 적절하게 Handling 하여 에러 처리를 구현하라.
    => Command 에서는 HttpError 클래스 사용 -> 업무에 사용되는 Exception 형식으로 새롭게 정의하기.

 


▶ 현재 작성한 구글 환불 API 코드 _〆(。。)

 

"""
    Server -> Google
    REST API테스트
    (구글 인앱 결제 환불)
    - Jira Ticket번호 : W**-****
    -종류
        1. revoke :구독 즉시 취소 및 환불
        (앱에서도 '해지 신청완료/결제중지'라는 문구가 뜬다)
        (이미 취소된 경우 error를 반환한다)
        (문제점? - 앱에서 '해지취소'를 누르면
            Play스토어 정기결제 창으로 가는데 아무것도 없다 -해지취소 불가?)
        2. refund :구독 지속 및 환불
        (앱에는 아무런 변화가 없고 '돈만 환불'된다)
        (문제점? -이미 환불된 건에 대해서도 error를 반환하지 않는다)
"""

from flask_script import Command as BaseCommand, Option

import googleapiclient
from googleapiclient.errors import HttpError
from flask import request, current_app, jsonify, abort

from google.oauth2 import service_account


class Command(BaseCommand):

    option_list = (
    	Option('--type', '-t', type=str, help='revoke/refund'),
        )

	def run(self, **options):
		""" google환불 API (revoke, refund)작동을 확인합니다. """
		func_type = options.get('type')

		if not func_type:
    		print(f'error - 타입 미입력.')
			return
        
    	# 환불할 구독 정보
   	 	package_name = '*'
    	subscription_id = '*'
    
   		 # 테스트에 사용한 Purchase Token 들
    	token_kakao = '*'
    	token_naver = '*'
    	token_030700 = '*'
    	token_030701 = '*'
    	token_030800 = '*'

		if func_type == "revoke":
    		info = google_revoke_api(package_name, subscription_id, token_030800)
    	elif func_type == "refund":
        	info = google_refund_api(package_name, subscription_id, token_030800)
		else:
        	info ={"result": None, "error": "No Function"}

		return info


def google_revoke_api(package_name, subscription_id, purchase_token):
	"""
		구글 환불 API기능 확인하기 (구독 즉시 취소)
        error처리를 감안했을 때, 'revoke'를 사용하는 것이 더 안정적
    """

	try:
    	scopes =['https://www.googleapis.com/auth/androidpublisher']
		service_account_info = current_app.config.get('GOOGLE_CREDENTIAL')
		credentials = service_account.Credentials.from_service_account_info(
		info=service_account_info, scopes=scopes)

		client = googleapiclient.discovery.build('androidpublisher', 'v3', credentials=credentials)
		revoke_query = client.purchases().subscriptions()\
        		.revoke(packageName=package_name, subscriptionId=subscription_id, token=purchase_token)
		revoke_query.execute()

	except Exception as e:
        # Http Error 인 경우 Handling
        # 예를 들어 이미 환불된 Token 에 revoke 요청을 넣은 경우
        # 에러 코드 400과 함께 상세 메시지를 던져 준다.
        if isinstance(e, HttpError):
            status = e.resp.status
            reason = e._get_reason()
			print(f'[{status}]{reason}')
			e ={'status': status, 'message': reason}
		else:
            print(f"error -{e}")
		info ={'result':{'is_revoke': False}, 'error': e}
	else:
        info ={'result':{'is_revoke': True}, 'error': None}

	return info


def google_refund_api(package_name, subscription_id, purchase_token):
	"""
		구글 환불 API기능 확인하기 (구독 지속)
    """

	try:
        scopes =['https://www.googleapis.com/auth/androidpublisher']
	service_account_info = current_app.config.get('GOOGLE_CREDENTIAL')
	credentials = service_account.Credentials.from_service_account_info(
	info=service_account_info, scopes=scopes)

	client = googleapiclient.discovery.build('androidpublisher', 'v3', credentials=credentials)
	refund_query = client.purchases().subscriptions()\
            .refund(packageName=package_name, subscriptionId=subscription_id, token=purchase_token)
	refund_query.execute()

	except Exception as e:
        # Http Error 인 경우 Handling
        # 단, refund 는 revoke 와 다르게 이미 환불된 건에 대해서
        # error 를 반환하지 않는다
        if isinstance(e, HttpError):
            status = e.resp.status
            reason = e._get_reason()
			print(f'[{status}]{reason}')
			e ={'status': status, 'message': reason}
		else:
            print(f"error -{e}")
        info ={'result':{'is_refund': False}, 'error': e}
	else:
        info ={'result':{'is_refund': True}, 'error': None}

	return info

 

<테스트 진행 방식>

0. Command 형태로 구글 환불 API 기능 작동을 확인하는 함수를 만든다.

1. 상용 서버에 구글 인앱 결제를 올린다. (테스트 안드로이드 기기 활용)

2. 상용 서버 DB (read 전용 replica) 에 접속하여 해당 purchase_token 값을 얻어낸다.

3. 해당 Token 값으로 Command 함수를 동작시켜 구글 환불 API 기능이 정상적으로 작동하는지 확인한다.

4. '환불이 이미 된 구독 정보' 에 중복으로 환불 요청이 들어간 경우에 대해 Error 를 확인한다.

5. 디버깅 모드로 Error 객체를 확인 - try/except 구조로 Error Handling 처리를 진행한다.

6. 여러 번의 테스트를 거치며 이상이 없는지 확인한다.

 

<핵심 코드>

revoke_query = client.purchases().subscriptions().revoke(packageName=package_name, subscriptionId=subscription_id, token=purchase_token)

refund_query = client.purchases().subscriptions().refund(packageName=package_name, subscriptionId=subscription_id, token=purchase_token)


=> 구글 API 공식 문서 목차 depth 구조와 동일하게 API 호출 코드가 구성되어 있다.
=> 해당 패턴을 인지하고 공식 문서를 보면 구글 API 를 간단하게 사용할 수 있다.

 

<에러 처리>

경우에 따라 HttpError 가 발생한다.
'googleapiclient' 라이브러리에서 HttpError 클래스 구조를 상세하게 보면
Error Handling 흐름을 알 수 있다. (디버깅 단계로 객체 확인해도 OK)

if isinstance(e, HttpError):
        status = e.resp.status
        reason = e._get_reason()
        print(f'[{status}]{reason}')
        e ={'status': status, 'message': reason}

=> HttpError 에 해당하는 에러가 발생할 경우
     상태 코드와 에러 메시지를 반환하도록 구성하였다.

=> 이미 환불된 건에 대해 중복으로 환불 요청이 들어갈 경우
     다음과 같은 응답이 전달된다.

{'result': {'is_revoke': False}, 'error': {'status': 400, 'message': 'The subscription cannot be refunded or revoked because it has expired.'}}

 


▶ 구글 환불 API (구독 관련)

 

https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions/revoke

 

Method: purchases.subscriptions.revoke  |  Google Play Developer API  |  Google Developers

Method: purchases.subscriptions.revoke Refunds and immediately revokes a user's subscription purchase. Access to the subscription will be terminated immediately and it will stop recurring. HTTP request POST https://androidpublisher.googleapis.com/androidpu

developers.google.com

  • revoke 는 구독이 즉시 해제되고 기간 상관없이 환불이 가능하다.
https://androidpublisher.googleapis.com/androidpublisher/v3/
applications/{packageName}/purchases/
subscriptions/{subscriptionId}/tokens/{token}:revoke

 

https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions/refund

 

Method: purchases.subscriptions.refund  |  Google Play Developer API  |  Google Developers

Method: purchases.subscriptions.refund Refunds a user's subscription purchase, but the subscription remains valid until its expiration time and it will continue to recur. HTTP request POST https://androidpublisher.googleapis.com/androidpublisher/v3/applica

developers.google.com

  • refund 는 구독은 유지되고 기관 상관없이 (?) 환불이 진행된다.
  • 환불이 이미 된 건에 대해서 요청이 들어가도 error 를 반환하지 않는다.
  • 여러가지 error 케이스에 대한 Handling 이 들어가는 경우에는 revoke 가 더 적합해 보인다.
https://androidpublisher.googleapis.com/androidpublisher/v3/
applications/{packageName}/purchases/
subscriptions/{subscriptionId}/tokens/{token}:refund

 

 


▶ To do

다음 업무!

 


Flask 기반의 파이썬 웹 프로그래밍 Chapter 2

 

Chapter 2. Flask 시작하기 - 노트 정리

 

내일 챕터 2 마저 읽고 티스토리에 정리하기 !