diff --git a/README.md b/README.md index 6c37be0..0b74343 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Python 언어로 작성된 어플리케이션, 프레임워크 등에서 사용 * PG 결제창 연동은 클라이언트 라이브러리에서 수행됩니다. (Javascript, Android, iOS, React Native, Flutter 등) * 결제 검증 및 취소, 빌링키 발급, 본인인증 등의 수행은 서버사이드에서 진행됩니다. (Java, PHP, Python, Ruby, Node.js, Go, ASP.NET 등) -## 목차 -- [사용하기](#사용하기) +## 목차 +- [PG API 사용하기](#사용하기) - [1. 토큰 발급](#1-토큰-발급) - [2. 결제 단건 조회](#2-결제-단건-조회) - [3. 결제 취소 (전액 취소 / 부분 취소)](#3-결제-취소-전액-취소--부분-취소) @@ -29,6 +29,13 @@ Python 언어로 작성된 어플리케이션, 프레임워크 등에서 사용 - [9-2. 현금영수증 발행 취소](#9-2-현금영수증-발행-취소) - [9-3. 별건 현금영수증 발행](#9-3-별건-현금영수증-발행) - [9-4. 별건 현금영수증 발행 취소](#9-4-별건-현금영수증-발행-취소) +- [Commerce API 사용하기](#10-commerce-api) + - [10-1. Commerce API 초기화](#10-1-commerce-api-초기화) + - [10-2. 사용자 관리](#10-2-사용자-관리) + - [10-3. 상품 관리](#10-3-상품-관리) + - [10-4. 주문 관리](#10-4-주문-관리) + - [10-5. 정기구독 관리](#10-5-정기구독-관리) + - [10-6. 청구서 관리](#10-6-청구서-관리) - [Example 프로젝트](#example-프로젝트) - [Documentation](#documentation) - [기술문의](#기술문의) @@ -369,6 +376,134 @@ if 'error_code' not in token: print(response) ``` +## 10. Commerce API + +부트페이 Commerce API를 사용하여 사용자, 상품, 주문, 정기구독 등을 관리할 수 있습니다. + +### 10-1. Commerce API 초기화 + +```python +from bootpay_backend.commerce import BootpayCommerce + +commerce = BootpayCommerce( + client_key='hxS-Up--5RvT6oU6QJE0JA', + secret_key='r5zxvDcQJiAP2PBQ0aJjSHQtblNmYFt6uFoEMhti_mg=', + mode='development' # 'production' | 'development' | 'stage' +) + +# 토큰 발급 +commerce.get_access_token() +``` + +### 10-2. 사용자 관리 + +```python +# 사용자 목록 조회 +users = commerce.user.list({'page': 1, 'limit': 10}) + +# 사용자 상세 조회 +user = commerce.user.detail('USER_ID') + +# 회원가입 +new_user = commerce.user.join({ + 'login_id': 'test@example.com', + 'login_pw': 'password123', + 'name': '홍길동', + 'email': 'test@example.com', + 'phone': '010-1234-5678' +}) + +# 사용자 정보 수정 +updated_user = commerce.user.update({ + 'user_id': 'USER_ID', + 'name': '수정된 이름' +}) +``` + +### 10-3. 상품 관리 + +```python +# 상품 목록 조회 +products = commerce.product.list({'page': 1, 'limit': 10}) + +# 상품 생성 +product = commerce.product.create({ + 'name': '테스트 상품', + 'price': 10000, + 'description': '상품 설명' +}) + +# 상품 상세 조회 +product_detail = commerce.product.detail('PRODUCT_ID') + +# 상품 수정 +updated_product = commerce.product.update({ + 'product_id': 'PRODUCT_ID', + 'name': '수정된 상품명', + 'price': 15000 +}) +``` + +### 10-4. 주문 관리 + +```python +# 주문 목록 조회 +orders = commerce.order.list({'page': 1, 'limit': 10}) + +# 주문 상세 조회 +order = commerce.order.detail('ORDER_ID') + +# 월별 주문 조회 +month_orders = commerce.order.month('USER_GROUP_ID', '2024-12') +``` + +### 10-5. 정기구독 관리 + +```python +# 정기구독 목록 조회 +subscriptions = commerce.order_subscription.list() + +# 정기구독 상세 조회 +subscription = commerce.order_subscription.detail('ORDER_SUBSCRIPTION_ID') + +# 정기구독 일시정지 +commerce.order_subscription.pause({ + 'order_subscription_id': 'ORDER_SUBSCRIPTION_ID', + 'pause_days': 30, + 'reason': '일시정지 사유' +}) + +# 정기구독 재개 +commerce.order_subscription.resume({ + 'order_subscription_id': 'ORDER_SUBSCRIPTION_ID' +}) + +# 정기구독 해지 +commerce.order_subscription.termination({ + 'order_subscription_id': 'ORDER_SUBSCRIPTION_ID', + 'reason': '해지 사유' +}) +``` + +### 10-6. 청구서 관리 + +```python +# 청구서 목록 조회 +invoices = commerce.invoice.list() + +# 청구서 생성 +invoice = commerce.invoice.create({ + 'user_id': 'USER_ID', + 'amount': 50000, + 'title': '청구서 제목' +}) + +# 청구서 알림 전송 +commerce.invoice.notify('INVOICE_ID', [1, 2]) # 1: SMS, 2: Email +``` + +더 자세한 Commerce API 사용 예제는 [test/commerce](./test/commerce) 디렉토리를 참고해주세요. + ## Example 프로젝트 [적용한 샘플 프로젝트](https://github.com/bootpay/backend-python-example)을 참조해주세요 diff --git a/bootpay_backend/commerce/__init__.py b/bootpay_backend/commerce/__init__.py index 9856c11..13c6d01 100644 --- a/bootpay_backend/commerce/__init__.py +++ b/bootpay_backend/commerce/__init__.py @@ -9,7 +9,8 @@ OrderCancelModule, OrderSubscriptionModule, OrderSubscriptionBillModule, - OrderSubscriptionAdjustmentModule + OrderSubscriptionAdjustmentModule, + StoreModule ) from .types import * @@ -64,6 +65,7 @@ def _init_modules(self): self.order_subscription = OrderSubscriptionModule(self) self.order_subscription_bill = OrderSubscriptionBillModule(self) self.order_subscription_adjustment = OrderSubscriptionAdjustmentModule(self) + self.store = StoreModule(self) def get_access_token(self): """ @@ -188,6 +190,7 @@ def clear_role(self): 'OrderSubscriptionModule', 'OrderSubscriptionBillModule', 'OrderSubscriptionAdjustmentModule', + 'StoreModule', # Types 'ListParams', 'CommerceAddress', diff --git a/bootpay_backend/commerce/modules/__init__.py b/bootpay_backend/commerce/modules/__init__.py index 1f4a3a6..6db4306 100644 --- a/bootpay_backend/commerce/modules/__init__.py +++ b/bootpay_backend/commerce/modules/__init__.py @@ -8,6 +8,7 @@ from .order_subscription import OrderSubscriptionModule from .order_subscription_bill import OrderSubscriptionBillModule from .order_subscription_adjustment import OrderSubscriptionAdjustmentModule +from .store import StoreModule __all__ = [ 'UserModule', @@ -18,5 +19,6 @@ 'OrderCancelModule', 'OrderSubscriptionModule', 'OrderSubscriptionBillModule', - 'OrderSubscriptionAdjustmentModule' + 'OrderSubscriptionAdjustmentModule', + 'StoreModule' ] diff --git a/bootpay_backend/commerce/modules/order_subscription.py b/bootpay_backend/commerce/modules/order_subscription.py index 797bde7..4adbb51 100644 --- a/bootpay_backend/commerce/modules/order_subscription.py +++ b/bootpay_backend/commerce/modules/order_subscription.py @@ -11,7 +11,12 @@ OrderSubscriptionPauseParams, OrderSubscriptionResumeParams, OrderSubscriptionTerminationParams, - CalcTerminateFeeResponse + CalcTerminateFeeResponse, + SupervisorOrderSubscriptionApproveParams, + SupervisorOrderSubscriptionRejectParams, + SupervisorOrderSubscriptionTerminateParams, + SupervisorOrderSubscriptionPauseParams, + SupervisorOrderSubscriptionResumeParams ) @@ -130,3 +135,18 @@ def update(self, params: OrderSubscriptionUpdateParams): if not params.get('order_subscription_id'): raise ValueError('order_subscription_id is required') return self._bootpay.put(f'order_subscriptions/{params["order_subscription_id"]}', params) + + def supervisor_approve(self, order_subscription_id: str, params: Optional[SupervisorOrderSubscriptionApproveParams] = None): + return self._bootpay.put(f'order_subscriptions/{order_subscription_id}/approve', params or {}) + + def supervisor_reject(self, order_subscription_id: str, params: Optional[SupervisorOrderSubscriptionRejectParams] = None): + return self._bootpay.put(f'order_subscriptions/{order_subscription_id}/reject', params or {}) + + def supervisor_terminate(self, order_subscription_id: str, params: Optional[SupervisorOrderSubscriptionTerminateParams] = None): + return self._bootpay.put(f'order_subscriptions/{order_subscription_id}/terminate', params or {}) + + def supervisor_pause(self, order_subscription_id: str, params: SupervisorOrderSubscriptionPauseParams): + return self._bootpay.put(f'order_subscriptions/{order_subscription_id}/pause', params) + + def supervisor_resume(self, order_subscription_id: str, params: Optional[SupervisorOrderSubscriptionResumeParams] = None): + return self._bootpay.put(f'order_subscriptions/{order_subscription_id}/resume', params or {}) diff --git a/bootpay_backend/commerce/modules/product.py b/bootpay_backend/commerce/modules/product.py index 8eb37de..f2611c6 100644 --- a/bootpay_backend/commerce/modules/product.py +++ b/bootpay_backend/commerce/modules/product.py @@ -47,6 +47,10 @@ def list(self, params: Optional[ProductListParams] = None): query = urlencode(query_params) if query_params else '' return self._bootpay.get(f'products{"?" + query if query else ""}') + def products(self, params: Optional[ProductListParams] = None): + """상품 목록 조회 (Mall API alias)""" + return self.list(params) + def create(self, product: CommerceProduct, image_paths: Optional[List[str]] = None): """ 상품 생성 (이미지 포함) @@ -64,6 +68,10 @@ def detail(self, product_id: str): """ return self._bootpay.get(f'products/{product_id}') + def product_detail(self, product_id: str): + """상품 상세 조회 (Mall API alias)""" + return self.detail(product_id) + def update(self, product: CommerceProduct): """ 상품 수정 diff --git a/bootpay_backend/commerce/modules/store.py b/bootpay_backend/commerce/modules/store.py new file mode 100644 index 0000000..3a888b9 --- /dev/null +++ b/bootpay_backend/commerce/modules/store.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..commerce_resource import BootpayCommerceResource + + +class StoreModule: + """스토어 모듈""" + + def __init__(self, bootpay: 'BootpayCommerceResource'): + self._bootpay = bootpay + + def get_store(self): + """가맹점 기본 정보 조회 (/v1/store)""" + return self._bootpay.get('store') + + def info(self): + return self.get_store() + + def get_store_detail(self): + """가맹점 상세 정보 조회 (/v1/store/detail)""" + return self._bootpay.get('store/detail') + + def detail(self): + return self.get_store_detail() diff --git a/bootpay_backend/commerce/modules/user.py b/bootpay_backend/commerce/modules/user.py index 555544e..3858f8f 100644 --- a/bootpay_backend/commerce/modules/user.py +++ b/bootpay_backend/commerce/modules/user.py @@ -65,6 +65,18 @@ def login(self, login_id: str, login_pw: str): 'login_pw': login_pw }) + def user_login(self, login_id: str, login_pw: str): + """회원 로그인 (Mall API alias)""" + return self.login(login_id, login_pw) + + def user_join(self, user: CommerceUser): + """회원가입 (Mall API alias)""" + return self.join(user) + + def user_join_check(self, check_type: str, pk: str): + """회원가입 중복 확인 (Mall API alias)""" + return self.check_exist(check_type, pk) + def list(self, params: Optional[UserListParams] = None): """ 사용자 목록 조회 diff --git a/bootpay_backend/commerce/types/__init__.py b/bootpay_backend/commerce/types/__init__.py index ddc81fa..a85ff1e 100644 --- a/bootpay_backend/commerce/types/__init__.py +++ b/bootpay_backend/commerce/types/__init__.py @@ -659,6 +659,33 @@ class CalcTerminateFeeResponse(TypedDict, total=False): final_fee: int +class SupervisorOrderSubscriptionApproveParams(TypedDict, total=False): + reason: str + + +class SupervisorOrderSubscriptionRejectParams(TypedDict, total=False): + reason: str + + +class SupervisorOrderSubscriptionTerminateParams(TypedDict, total=False): + reason: str + termination_fee: int + last_bill_refund_price: int + final_fee: int + service_end_at: str + cancel_date: str + + +class SupervisorOrderSubscriptionPauseParams(TypedDict, total=False): + reason: str + paused_at: str + expected_resume_at: str + + +class SupervisorOrderSubscriptionResumeParams(TypedDict, total=False): + reason: str + + # OrderSubscriptionBill Types class CommerceOrderSubscriptionBill(TypedDict, total=False): order_subscription_bill_id: str diff --git a/bootpay_backend/rest_client.py b/bootpay_backend/rest_client.py index 930d49b..897d086 100644 --- a/bootpay_backend/rest_client.py +++ b/bootpay_backend/rest_client.py @@ -1,5 +1,6 @@ import requests import urllib.parse +import base64 class BootpayBackend: @@ -11,9 +12,11 @@ class BootpayBackend: API_VERSION = '5.0.0' SDK_VERSION = '2.1.2' - def __init__(self, application_id, private_key, mode='production'): + def __init__(self, application_id, private_key, mode='production', client_key=None, secret_key=None): self.application_id = application_id self.private_key = private_key + self.client_key = client_key + self.secret_key = secret_key self.mode = mode self.token = None self.api_version = self.API_VERSION @@ -33,10 +36,18 @@ def set_api_version(self, version): # @param method: string, url: string, data: object, headers: object # @returns ResponseForamt def __request(self, method='', url='', data=None, headers={}, params={}): + auth_header = None + if self.client_key and self.secret_key: + encoded = base64.b64encode(f"{self.client_key}:{self.secret_key}".encode('utf-8')).decode('utf-8') + auth_header = f"Basic {encoded}" + elif self.application_id: + if self.token is not None: + auth_header = f"Bearer {self.token}" + if method in ['put', 'post']: response = getattr(requests, method)(url, json=data, headers=dict(headers, **{ 'Accept': 'application/json', - 'Authorization': (None if self.token is None else f"Bearer {self.token}"), + 'Authorization': auth_header, 'BOOTPAY-API-VERSION': self.api_version, 'BOOTPAY-SDK-VERSION': self.SDK_VERSION, 'BOOTPAY-SDK-TYPE': '302' @@ -44,7 +55,7 @@ def __request(self, method='', url='', data=None, headers={}, params={}): else: response = getattr(requests, method)(url, headers=dict(headers, **{ 'Accept': 'application/json', - 'Authorization': (None if self.token is None else f"Bearer {self.token}") + 'Authorization': auth_header }), params=params) return response.json() diff --git a/tests_basic_auth_product_info.py b/tests_basic_auth_product_info.py new file mode 100644 index 0000000..a43524c --- /dev/null +++ b/tests_basic_auth_product_info.py @@ -0,0 +1,22 @@ +import base64 +import os +import requests + +client_key = os.getenv('BP_CLIENT_KEY', 'QIzXk4M3EeD-6B1GTfmGHA') +secret_key = os.getenv('BP_SECRET_KEY', 'vRle44QfyBj7nzJlBbeebqkbtlJVRTS2DQa9Adpz3d8=') +base_url = os.getenv('BP_BASE_URL', 'https://dev-api.bootapi.com/v1') + +encoded = base64.b64encode(f"{client_key}:{secret_key}".encode()).decode() +headers = { + 'Authorization': f'Basic {encoded}', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'bootpay_api_version': '5.0.0', + 'bootpay_sdk_version': '5.0.0', + 'bootpay_sdk_type': '300', +} + +resp = requests.get(f"{base_url}/products?page=1&limit=1", headers=headers, timeout=20) +print({'status': resp.status_code, 'ok': resp.ok, 'preview': resp.text[:500]}) +if not resp.ok: + raise SystemExit(1)