bootpay-backend 2.2.0__tar.gz → 2.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/PKG-INFO +46 -6
  2. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/README.md +34 -4
  3. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/commerce/__init__.py +69 -2
  4. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/commerce/commerce_resource.py +59 -29
  5. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/commerce/modules/__init__.py +17 -1
  6. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/cart.py +29 -0
  7. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/category.py +92 -0
  8. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/coupon.py +51 -0
  9. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/invoice.py +106 -0
  10. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/mall_setting.py +63 -0
  11. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/commerce/modules/order.py +5 -0
  12. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/order_cancel.py +139 -0
  13. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/order_subscription.py +380 -0
  14. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/order_subscription_adjustment.py +95 -0
  15. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/commerce/modules/order_subscription_bill.py +32 -16
  16. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/order_subscription_request.py +131 -0
  17. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/point.py +43 -0
  18. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/product.py +217 -0
  19. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/store.py +45 -0
  20. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/user.py +232 -0
  21. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/commerce/modules/user_group.py +55 -9
  22. bootpay_backend-2.5.0/bootpay_backend/commerce/modules/webhook.py +34 -0
  23. bootpay_backend-2.5.0/bootpay_backend/commerce/types/__init__.py +1445 -0
  24. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/rest_client.py +164 -29
  25. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend.egg-info/PKG-INFO +47 -7
  26. bootpay_backend-2.5.0/bootpay_backend.egg-info/SOURCES.txt +58 -0
  27. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend.egg-info/top_level.txt +1 -0
  28. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/setup.py +1 -1
  29. bootpay_backend-2.5.0/tests/__init__.py +0 -0
  30. bootpay_backend-2.5.0/tests/commerce/__init__.py +0 -0
  31. bootpay_backend-2.5.0/tests/commerce/test_auth_contract.py +86 -0
  32. bootpay_backend-2.5.0/tests/commerce/test_mall_setting.py +20 -0
  33. bootpay_backend-2.5.0/tests/commerce/test_order.py +61 -0
  34. bootpay_backend-2.5.0/tests/commerce/test_order_subscription_charge.py +50 -0
  35. bootpay_backend-2.5.0/tests/commerce/test_product.py +75 -0
  36. bootpay_backend-2.5.0/tests/commerce/test_product_mall.py +27 -0
  37. bootpay_backend-2.5.0/tests/commerce/test_store.py +20 -0
  38. bootpay_backend-2.5.0/tests/commerce/test_token.py +65 -0
  39. bootpay_backend-2.5.0/tests/commerce/test_user.py +81 -0
  40. bootpay_backend-2.5.0/tests/commerce/test_user_mall.py +44 -0
  41. bootpay_backend-2.5.0/tests/commerce/test_webhook.py +20 -0
  42. bootpay_backend-2.5.0/tests/commerce/test_wire_format.py +563 -0
  43. bootpay_backend-2.5.0/tests/conftest.py +158 -0
  44. bootpay_backend-2.5.0/tests/pg/__init__.py +0 -0
  45. bootpay_backend-2.5.0/tests/pg/test_auth.py +66 -0
  46. bootpay_backend-2.5.0/tests/pg/test_billing.py +91 -0
  47. bootpay_backend-2.5.0/tests/pg/test_cash_receipt.py +59 -0
  48. bootpay_backend-2.5.0/tests/pg/test_legacy_compatibility.py +149 -0
  49. bootpay_backend-2.5.0/tests/pg/test_payment.py +45 -0
  50. bootpay_backend-2.5.0/tests/pg/test_sequential_billing_key.py +29 -0
  51. bootpay_backend-2.5.0/tests/pg/test_token.py +62 -0
  52. bootpay_backend-2.5.0/tests/pg/test_wallet.py +33 -0
  53. bootpay_backend-2.2.0/bootpay_backend/commerce/modules/invoice.py +0 -61
  54. bootpay_backend-2.2.0/bootpay_backend/commerce/modules/order_cancel.py +0 -77
  55. bootpay_backend-2.2.0/bootpay_backend/commerce/modules/order_subscription.py +0 -132
  56. bootpay_backend-2.2.0/bootpay_backend/commerce/modules/order_subscription_adjustment.py +0 -52
  57. bootpay_backend-2.2.0/bootpay_backend/commerce/modules/product.py +0 -93
  58. bootpay_backend-2.2.0/bootpay_backend/commerce/modules/user.py +0 -114
  59. bootpay_backend-2.2.0/bootpay_backend/commerce/types/__init__.py +0 -752
  60. bootpay_backend-2.2.0/bootpay_backend.egg-info/SOURCES.txt +0 -26
  61. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/LICENSE +0 -0
  62. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/__init__.py +0 -0
  63. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend/bankcode.py +0 -0
  64. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend.egg-info/dependency_links.txt +0 -0
  65. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend.egg-info/not-zip-safe +0 -0
  66. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/bootpay_backend.egg-info/requires.txt +0 -0
  67. {bootpay_backend-2.2.0 → bootpay_backend-2.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: bootpay_backend
3
- Version: 2.2.0
3
+ Version: 2.5.0
4
4
  Summary: bootpay server side plugin for python
5
5
  Home-page: https://github.com/bootpay/backend-python/tree/2-x-development
6
6
  Author: bootpay <bootpay.co.kr@gmail.com>
@@ -27,6 +27,16 @@ Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: requests>=2.20; python_version >= "3.0"
29
29
  Requires-Dist: requests[security]>=2.20; python_version < "3.0"
30
+ Dynamic: author
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: home-page
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
30
40
 
31
41
 
32
42
  ## Bootpay Python Server Side Library
@@ -70,13 +80,42 @@ Python 언어로 작성된 어플리케이션, 프레임워크 등에서 사용
70
80
  pip install bootpay-backend
71
81
  ```
72
82
 
83
+
84
+ ## 환경변수 설정
85
+
86
+ 예제와 테스트는 각 SDK 루트의 `.env` 파일을 우선 읽습니다. 먼저 `.env.example`을 복사한 뒤 필요한 키만 변경하세요. `.env`는 gitignore 처리되어 커밋되지 않습니다.
87
+
88
+ ```bash
89
+ cp .env.example .env
90
+ # BOOTPAY_ENV=production 또는 development
91
+ ```
92
+
93
+ 주요 변수:
94
+
95
+ ```env
96
+ BOOTPAY_ENV=production
97
+ BOOTPAY_PG_CLIENT_KEY_PROD=...
98
+ BOOTPAY_PG_SECRET_KEY_PROD=...
99
+ BOOTPAY_PG_CLIENT_KEY_DEV=...
100
+ BOOTPAY_PG_SECRET_KEY_DEV=...
101
+ BOOTPAY_COMMERCE_CLIENT_KEY_PROD=...
102
+ BOOTPAY_COMMERCE_SECRET_KEY_PROD=...
103
+ BOOTPAY_COMMERCE_CLIENT_KEY_DEV=...
104
+ BOOTPAY_COMMERCE_SECRET_KEY_DEV=...
105
+ ```
106
+
107
+ 변수가 없으면 SDK 테스트용 기본값(NodeJS 기준 ck/sk)으로 fallback 합니다.
108
+
73
109
  # 사용하기
74
110
 
111
+ > 권장 인증 방식은 `client_key/secret_key`입니다. 기존 positional `application_id/private_key` 방식도 계속 지원됩니다.
112
+
75
113
  ```python
76
114
 
115
+ import os
77
116
  from bootpay_backend import BootpayBackend
78
117
 
79
- bootpay = BootpayBackend('5b8f6a4d396fa665fdc2b5ea', 'rm6EYECr6aroQVG2ntW0A6LpWnkTgP4uQ3H18sDDUYw=')
118
+ bootpay = BootpayBackend(client_key=os.getenv('BOOTPAY_PG_CLIENT_KEY_PROD'), secret_key=os.getenv('BOOTPAY_PG_SECRET_KEY_PROD'))
80
119
 
81
120
  token = bootpay.get_access_token()
82
121
  if 'error_code' not in token:
@@ -91,7 +130,7 @@ if 'error_code' not in token:
91
130
  발급된 토큰은 30분간 유효하며, 최초 발급일로부터 30분이 지날 경우 토큰 발급 함수를 재호출 해주셔야 합니다.
92
131
 
93
132
  ```python
94
- bootpay = BootpayBackend('5b8f6a4d396fa665fdc2b5ea', 'rm6EYECr6aroQVG2ntW0A6LpWnkTgP4uQ3H18sDDUYw=')
133
+ bootpay = BootpayBackend(client_key=os.getenv('BOOTPAY_PG_CLIENT_KEY_PROD'), secret_key=os.getenv('BOOTPAY_PG_SECRET_KEY_PROD'))
95
134
 
96
135
  token = bootpay.get_access_token()
97
136
  if 'error_code' not in token:
@@ -405,11 +444,12 @@ if 'error_code' not in token:
405
444
 
406
445
  ## Documentation
407
446
 
408
- [부트페이 개발매뉴얼](https://developer.bootpay.co.kr/)을 참조해주세요
447
+ - [부트페이 결제 개발문서](https://developers.bootpay.ai)
448
+ - [부트페이 커머스 개발문서](https://commerce.bootpay.ai)
409
449
 
410
450
  ## 기술문의
411
451
 
412
- [부트페이 홈페이지](https://www.bootpay.co.kr) 우측 하단 채팅을 통해 기술문의 주세요!
452
+ [부트페이 홈페이지](https://www.bootpay.ai)에서 문의해주세요!
413
453
 
414
454
  ## License
415
455
 
@@ -40,13 +40,42 @@ Python 언어로 작성된 어플리케이션, 프레임워크 등에서 사용
40
40
  pip install bootpay-backend
41
41
  ```
42
42
 
43
+
44
+ ## 환경변수 설정
45
+
46
+ 예제와 테스트는 각 SDK 루트의 `.env` 파일을 우선 읽습니다. 먼저 `.env.example`을 복사한 뒤 필요한 키만 변경하세요. `.env`는 gitignore 처리되어 커밋되지 않습니다.
47
+
48
+ ```bash
49
+ cp .env.example .env
50
+ # BOOTPAY_ENV=production 또는 development
51
+ ```
52
+
53
+ 주요 변수:
54
+
55
+ ```env
56
+ BOOTPAY_ENV=production
57
+ BOOTPAY_PG_CLIENT_KEY_PROD=...
58
+ BOOTPAY_PG_SECRET_KEY_PROD=...
59
+ BOOTPAY_PG_CLIENT_KEY_DEV=...
60
+ BOOTPAY_PG_SECRET_KEY_DEV=...
61
+ BOOTPAY_COMMERCE_CLIENT_KEY_PROD=...
62
+ BOOTPAY_COMMERCE_SECRET_KEY_PROD=...
63
+ BOOTPAY_COMMERCE_CLIENT_KEY_DEV=...
64
+ BOOTPAY_COMMERCE_SECRET_KEY_DEV=...
65
+ ```
66
+
67
+ 변수가 없으면 SDK 테스트용 기본값(NodeJS 기준 ck/sk)으로 fallback 합니다.
68
+
43
69
  # 사용하기
44
70
 
71
+ > 권장 인증 방식은 `client_key/secret_key`입니다. 기존 positional `application_id/private_key` 방식도 계속 지원됩니다.
72
+
45
73
  ```python
46
74
 
75
+ import os
47
76
  from bootpay_backend import BootpayBackend
48
77
 
49
- bootpay = BootpayBackend('5b8f6a4d396fa665fdc2b5ea', 'rm6EYECr6aroQVG2ntW0A6LpWnkTgP4uQ3H18sDDUYw=')
78
+ bootpay = BootpayBackend(client_key=os.getenv('BOOTPAY_PG_CLIENT_KEY_PROD'), secret_key=os.getenv('BOOTPAY_PG_SECRET_KEY_PROD'))
50
79
 
51
80
  token = bootpay.get_access_token()
52
81
  if 'error_code' not in token:
@@ -61,7 +90,7 @@ if 'error_code' not in token:
61
90
  발급된 토큰은 30분간 유효하며, 최초 발급일로부터 30분이 지날 경우 토큰 발급 함수를 재호출 해주셔야 합니다.
62
91
 
63
92
  ```python
64
- bootpay = BootpayBackend('5b8f6a4d396fa665fdc2b5ea', 'rm6EYECr6aroQVG2ntW0A6LpWnkTgP4uQ3H18sDDUYw=')
93
+ bootpay = BootpayBackend(client_key=os.getenv('BOOTPAY_PG_CLIENT_KEY_PROD'), secret_key=os.getenv('BOOTPAY_PG_SECRET_KEY_PROD'))
65
94
 
66
95
  token = bootpay.get_access_token()
67
96
  if 'error_code' not in token:
@@ -375,11 +404,12 @@ if 'error_code' not in token:
375
404
 
376
405
  ## Documentation
377
406
 
378
- [부트페이 개발매뉴얼](https://developer.bootpay.co.kr/)을 참조해주세요
407
+ - [부트페이 결제 개발문서](https://developers.bootpay.ai)
408
+ - [부트페이 커머스 개발문서](https://commerce.bootpay.ai)
379
409
 
380
410
  ## 기술문의
381
411
 
382
- [부트페이 홈페이지](https://www.bootpay.co.kr) 우측 하단 채팅을 통해 기술문의 주세요!
412
+ [부트페이 홈페이지](https://www.bootpay.ai)에서 문의해주세요!
383
413
 
384
414
  ## License
385
415
 
@@ -9,7 +9,15 @@ from .modules import (
9
9
  OrderCancelModule,
10
10
  OrderSubscriptionModule,
11
11
  OrderSubscriptionBillModule,
12
- OrderSubscriptionAdjustmentModule
12
+ OrderSubscriptionAdjustmentModule,
13
+ StoreModule,
14
+ CategoryModule,
15
+ CouponModule,
16
+ PointModule,
17
+ CartModule,
18
+ OrderSubscriptionRequestModule,
19
+ MallSettingModule,
20
+ WebhookModule
13
21
  )
14
22
  from .types import *
15
23
 
@@ -49,7 +57,7 @@ class BootpayCommerce(BootpayCommerceResource):
49
57
  :param mode: 환경 ('development', 'stage', 'production')
50
58
  """
51
59
  super().__init__()
52
- if client_key and secret_key:
60
+ if client_key is not None or secret_key is not None:
53
61
  self.set_configuration(client_key, secret_key, mode)
54
62
  self._init_modules()
55
63
 
@@ -64,6 +72,14 @@ class BootpayCommerce(BootpayCommerceResource):
64
72
  self.order_subscription = OrderSubscriptionModule(self)
65
73
  self.order_subscription_bill = OrderSubscriptionBillModule(self)
66
74
  self.order_subscription_adjustment = OrderSubscriptionAdjustmentModule(self)
75
+ self.store = StoreModule(self)
76
+ self.category = CategoryModule(self)
77
+ self.coupon = CouponModule(self)
78
+ self.point = PointModule(self)
79
+ self.cart = CartModule(self)
80
+ self.order_subscription_request = OrderSubscriptionRequestModule(self)
81
+ self.mall_setting = MallSettingModule(self)
82
+ self.webhook = WebhookModule(self)
67
83
 
68
84
  def get_access_token(self):
69
85
  """
@@ -188,6 +204,14 @@ __all__ = [
188
204
  'OrderSubscriptionModule',
189
205
  'OrderSubscriptionBillModule',
190
206
  'OrderSubscriptionAdjustmentModule',
207
+ 'StoreModule',
208
+ 'CategoryModule',
209
+ 'CouponModule',
210
+ 'PointModule',
211
+ 'CartModule',
212
+ 'OrderSubscriptionRequestModule',
213
+ 'MallSettingModule',
214
+ 'WebhookModule',
191
215
  # Types
192
216
  'ListParams',
193
217
  'CommerceAddress',
@@ -242,4 +266,47 @@ __all__ = [
242
266
  'SUBSCRIPTION_ADJUSTMENT_TYPE_PERIOD_DISCOUNT',
243
267
  'CommerceOrderSubscriptionAdjustment',
244
268
  'OrderSubscriptionAdjustmentUpdateParams',
269
+ 'SupervisorOrderSubscriptionApproveParams',
270
+ 'SupervisorOrderSubscriptionRejectParams',
271
+ 'SupervisorOrderSubscriptionTerminateParams',
272
+ 'SupervisorOrderSubscriptionPauseParams',
273
+ 'SupervisorOrderSubscriptionResumeParams',
274
+ 'CommerceCategory',
275
+ 'CategoryCreateParams',
276
+ 'CategoryUpdateParams',
277
+ 'CommerceCoupon',
278
+ 'CouponListParams',
279
+ 'CouponDownloadParams',
280
+ 'PointBalance',
281
+ 'PointTransaction',
282
+ 'PointTransactionsResponse',
283
+ 'PointTransactionsParams',
284
+ 'CartItemPayload',
285
+ 'ShippingAddressPayload',
286
+ 'OrderPreviewParams',
287
+ 'DeliveryGroupItem',
288
+ 'DeliveryGroup',
289
+ 'AppliedCouponSnapshot',
290
+ 'OrderPreviewSummary',
291
+ 'OrderPreviewUnavailableItem',
292
+ 'OrderPreviewResponse',
293
+ 'OrderSubscriptionRequest',
294
+ 'OrderSubscriptionRequestListParams',
295
+ 'OrderSubscriptionRequestUpdateParams',
296
+ 'InvoiceListResponse',
297
+ 'OrderCancelWithdrawParams',
298
+ 'OrderSubscriptionPurchaseParams',
299
+ 'OrderSubscriptionTransferParams',
300
+ 'SupervisorOrderSubscriptionChargeParams',
301
+ 'SupervisorOrderSubscriptionChargeRevokeParams',
302
+ 'OrderSubscriptionChargeResponse',
303
+ 'OrderSubscriptionChargeRevokeResponse',
304
+ 'MallProductListParams',
305
+ 'MallUserLoginParams',
306
+ 'MallUserJoinParams',
307
+ 'MallUserJoinCheckType',
308
+ 'MallUserSessionResponse',
309
+ 'MallSettingUpdateParams',
310
+ 'CommerceMallSetting',
311
+ 'SendTestWebhookParams',
245
312
  ]
@@ -53,19 +53,28 @@ class BootpayCommerceResource:
53
53
 
54
54
  def _get_basic_auth_header(self) -> str:
55
55
  """Basic Auth 헤더 생성"""
56
- if self.client_key and self.secret_key:
57
- credentials = f'{self.client_key}:{self.secret_key}'
58
- encoded = base64.b64encode(credentials.encode()).decode()
59
- return f'Basic {encoded}'
60
- return ''
56
+ has_client_key = bool(self.client_key)
57
+ has_secret_key = bool(self.secret_key)
58
+ if not has_client_key and not has_secret_key:
59
+ raise ValueError('Commerce API에는 client_key와 secret_key가 필요합니다.')
60
+ if has_client_key != has_secret_key:
61
+ missing = 'secret_key' if has_client_key else 'client_key'
62
+ raise ValueError(f'{missing} 값이 비어있습니다. client_key와 secret_key는 함께 지정해야 합니다.')
63
+ credentials = f'{self.client_key}:{self.secret_key}'
64
+ encoded = base64.b64encode(credentials.encode()).decode()
65
+ return f'Basic {encoded}'
61
66
 
62
67
  def _entrypoints(self, url: str) -> str:
63
68
  """엔트리포인트 URL 생성"""
64
69
  return '/'.join([self.API_ENTRYPOINTS[self.mode], url])
65
70
 
66
- def _get_headers(self, include_auth: bool = True) -> Dict[str, str]:
67
- """공통 헤더 생성"""
68
- headers = {
71
+ def _get_headers(self, include_auth: bool = True, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
72
+ """
73
+ 공통 헤더 생성
74
+ 요청별로 지정된 헤더(headers)가 공통값을 덮어쓴다 —
75
+ supervisor 전용 endpoint 의 BOOTPAY-ROLE 을 공통 계층이 덮어쓰지 않기 위함.
76
+ """
77
+ merged = {
69
78
  'Content-Type': 'application/json',
70
79
  'Accept': 'application/json',
71
80
  'Accept-Charset': 'utf-8',
@@ -74,25 +83,29 @@ class BootpayCommerceResource:
74
83
  'BOOTPAY-SDK-TYPE': '302',
75
84
  'BOOTPAY-ROLE': self._role or 'user'
76
85
  }
77
- if include_auth and self._token:
78
- headers['Authorization'] = f'Bearer {self._token}'
79
- return headers
80
-
81
- def get(self, url: str, params: Optional[Dict] = None):
86
+ if include_auth:
87
+ basic_auth = self._get_basic_auth_header()
88
+ if basic_auth:
89
+ merged['Authorization'] = basic_auth
90
+ if headers:
91
+ merged.update(headers)
92
+ return merged
93
+
94
+ def get(self, url: str, params: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
82
95
  """GET 요청"""
83
96
  response = requests.get(
84
97
  self._entrypoints(url),
85
- headers=self._get_headers(),
98
+ headers=self._get_headers(headers=headers),
86
99
  params=params,
87
100
  timeout=self.timeout
88
101
  )
89
102
  return response.json()
90
103
 
91
- def post(self, url: str, data: Optional[Dict] = None):
104
+ def post(self, url: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
92
105
  """POST 요청"""
93
106
  response = requests.post(
94
107
  self._entrypoints(url),
95
- headers=self._get_headers(),
108
+ headers=self._get_headers(headers=headers),
96
109
  json=data,
97
110
  timeout=self.timeout
98
111
  )
@@ -110,14 +123,18 @@ class BootpayCommerceResource:
110
123
  )
111
124
  return response.json()
112
125
 
113
- def post_multipart(self, url: str, data: Dict, image_paths: Optional[List[str]] = None):
126
+ def post_multipart(self, url: str, data: Dict, image_paths: Optional[List[str]] = None,
127
+ headers: Optional[Dict[str, str]] = None):
114
128
  """
115
129
  Multipart/form-data POST 요청 (이미지 업로드 포함)
130
+ ⚠️ Content-Type 은 requests 가 생성한 값(boundary 포함)을 그대로 사용한다.
131
+ 직접 지정하면 boundary 가 사라져 본문이 서버에서 null 로 파싱된다.
116
132
  :param url: 요청 URL
117
133
  :param data: 폼 데이터
118
134
  :param image_paths: 이미지 파일 경로 배열
135
+ :param headers: 요청별 추가 헤더 (공통값을 덮어쓴다)
119
136
  """
120
- headers = {
137
+ request_headers = {
121
138
  'Accept': 'application/json',
122
139
  'Accept-Charset': 'utf-8',
123
140
  'BOOTPAY-SDK-VERSION': self.SDK_VERSION,
@@ -125,29 +142,37 @@ class BootpayCommerceResource:
125
142
  'BOOTPAY-SDK-TYPE': '302',
126
143
  'BOOTPAY-ROLE': self._role or 'user'
127
144
  }
128
- if self._token:
129
- headers['Authorization'] = f'Bearer {self._token}'
145
+ basic_auth = self._get_basic_auth_header()
146
+ if basic_auth:
147
+ request_headers['Authorization'] = basic_auth
148
+ if headers:
149
+ request_headers.update(headers)
130
150
 
131
151
  # 폼 데이터 준비
152
+ # ⚠️ bool 은 소문자 'true'/'false' 로 보낸다 — str(True) 의 'True'/'False' 는
153
+ # Rails 캐스팅 대상이 아니라서 "False" 가 true 로 해석되는 실위험이 있다.
132
154
  form_data = {}
133
155
  for key, value in data.items():
134
156
  if value is not None:
135
- if isinstance(value, (dict, list)):
157
+ if isinstance(value, bool):
158
+ form_data[key] = 'true' if value else 'false'
159
+ elif isinstance(value, (dict, list)):
136
160
  form_data[key] = json.dumps(value)
137
161
  else:
138
162
  form_data[key] = str(value)
139
163
 
140
164
  # 파일 준비
165
+ # ⚠️ Rails 는 반복된 `images` 를 배열로 받지 않는다. images[0], images[1] ... 로 인덱싱해야 한다.
141
166
  files = []
142
167
  if image_paths:
143
- for image_path in image_paths:
168
+ for index, image_path in enumerate(image_paths):
144
169
  filename = os.path.basename(image_path)
145
- files.append(('images', (filename, open(image_path, 'rb'))))
170
+ files.append((f'images[{index}]', (filename, open(image_path, 'rb'))))
146
171
 
147
172
  try:
148
173
  response = requests.post(
149
174
  self._entrypoints(url),
150
- headers=headers,
175
+ headers=request_headers,
151
176
  data=form_data,
152
177
  files=files if files else None,
153
178
  timeout=self.timeout
@@ -158,22 +183,27 @@ class BootpayCommerceResource:
158
183
  for _, file_tuple in files:
159
184
  file_tuple[1].close()
160
185
 
161
- def put(self, url: str, data: Optional[Dict] = None):
186
+ def put(self, url: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
162
187
  """PUT 요청"""
163
188
  response = requests.put(
164
189
  self._entrypoints(url),
165
- headers=self._get_headers(),
190
+ headers=self._get_headers(headers=headers),
166
191
  json=data,
167
192
  timeout=self.timeout
168
193
  )
169
194
  return response.json()
170
195
 
171
- def delete(self, url: str, params: Optional[Dict] = None):
172
- """DELETE 요청"""
196
+ def delete(self, url: str, params: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None,
197
+ data: Optional[Dict] = None):
198
+ """
199
+ DELETE 요청
200
+ :param data: 요청 body (JSON) — 대상 ID 를 query 가 아닌 body 로 보내는 endpoint 용
201
+ """
173
202
  response = requests.delete(
174
203
  self._entrypoints(url),
175
- headers=self._get_headers(),
204
+ headers=self._get_headers(headers=headers),
176
205
  params=params,
206
+ json=data,
177
207
  timeout=self.timeout
178
208
  )
179
209
  return response.json()
@@ -8,6 +8,14 @@ from .order_cancel import OrderCancelModule
8
8
  from .order_subscription import OrderSubscriptionModule
9
9
  from .order_subscription_bill import OrderSubscriptionBillModule
10
10
  from .order_subscription_adjustment import OrderSubscriptionAdjustmentModule
11
+ from .store import StoreModule
12
+ from .category import CategoryModule
13
+ from .coupon import CouponModule
14
+ from .point import PointModule
15
+ from .cart import CartModule
16
+ from .order_subscription_request import OrderSubscriptionRequestModule
17
+ from .mall_setting import MallSettingModule
18
+ from .webhook import WebhookModule
11
19
 
12
20
  __all__ = [
13
21
  'UserModule',
@@ -18,5 +26,13 @@ __all__ = [
18
26
  'OrderCancelModule',
19
27
  'OrderSubscriptionModule',
20
28
  'OrderSubscriptionBillModule',
21
- 'OrderSubscriptionAdjustmentModule'
29
+ 'OrderSubscriptionAdjustmentModule',
30
+ 'StoreModule',
31
+ 'CategoryModule',
32
+ 'CouponModule',
33
+ 'PointModule',
34
+ 'CartModule',
35
+ 'OrderSubscriptionRequestModule',
36
+ 'MallSettingModule',
37
+ 'WebhookModule'
22
38
  ]
@@ -0,0 +1,29 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ if TYPE_CHECKING:
4
+ from ..commerce_resource import BootpayCommerceResource
5
+
6
+ from ..types import (
7
+ OrderPreviewParams,
8
+ OrderPreviewResponse
9
+ )
10
+
11
+
12
+ class CartModule:
13
+ """장바구니 모듈"""
14
+
15
+ def __init__(self, bootpay: 'BootpayCommerceResource'):
16
+ self._bootpay = bootpay
17
+
18
+ def order_preview(self, params: Optional[OrderPreviewParams] = None):
19
+ """
20
+ 주문 미리보기 (배송비/할인 권위적 계산)
21
+ POST /v1/cart/order-preview
22
+
23
+ member_mode='guest' (기본): cart_items 필수
24
+ member_mode='member': 서버 장바구니 사용 (user 토큰 필요)
25
+
26
+ :param params: 주문 미리보기 파라미터
27
+ :return: OrderPreviewResponse
28
+ """
29
+ return self._bootpay.post('cart/order-preview', params if params is not None else {})
@@ -0,0 +1,92 @@
1
+ import uuid
2
+ from typing import TYPE_CHECKING, Dict, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from ..commerce_resource import BootpayCommerceResource
6
+
7
+ from ..types import (
8
+ CommerceCategory,
9
+ CategoryCreateParams,
10
+ CategoryUpdateParams
11
+ )
12
+
13
+
14
+ class CategoryModule:
15
+ """카테고리 모듈"""
16
+
17
+ def __init__(self, bootpay: 'BootpayCommerceResource'):
18
+ self._bootpay = bootpay
19
+
20
+ def list(self):
21
+ """
22
+ 카테고리 트리 조회
23
+ :return: List[CommerceCategory]
24
+ """
25
+ return self._bootpay.get('categories')
26
+
27
+ def detail(self, category_id: str):
28
+ """
29
+ 카테고리 단건 조회
30
+ :param category_id: 카테고리 ID
31
+ :return: CommerceCategory
32
+ """
33
+ return self._bootpay.get(f'categories/{category_id}')
34
+
35
+ def create(self, params: CategoryCreateParams):
36
+ """
37
+ 카테고리 생성
38
+ POST /v1/categories
39
+ ⚠️ 서버가 supervisor scope 를 요구한다 (scope_invalid!).
40
+ :param params: 카테고리 생성 파라미터
41
+ :return: CommerceCategory
42
+ """
43
+ params = dict(params or {})
44
+ idempotency_key = params.pop('idempotency_key', None)
45
+ return self._bootpay.post(
46
+ 'categories',
47
+ params,
48
+ headers=self._supervisor_headers(idempotency_key)
49
+ )
50
+
51
+ def update(self, params: CategoryUpdateParams):
52
+ """
53
+ 카테고리 수정
54
+ PUT /v1/categories/{category_id}
55
+ ⚠️ 서버가 supervisor scope 를 요구한다 (scope_invalid!).
56
+ :param params: 카테고리 수정 파라미터 (category_id 필수)
57
+ :return: CommerceCategory
58
+ """
59
+ if not params.get('category_id'):
60
+ raise ValueError('category_id is required')
61
+ params = dict(params)
62
+ category_id = params.pop('category_id')
63
+ idempotency_key = params.pop('idempotency_key', None)
64
+ return self._bootpay.put(
65
+ f'categories/{category_id}',
66
+ params,
67
+ headers=self._supervisor_headers(idempotency_key)
68
+ )
69
+
70
+ def destroy(self, category_id: str, idempotency_key: Optional[str] = None):
71
+ """
72
+ 카테고리 삭제
73
+ DELETE /v1/categories/{category_id}
74
+ ⚠️ 서버가 supervisor scope 를 요구한다 (scope_invalid!).
75
+ :param category_id: 카테고리 ID
76
+ :param idempotency_key: 미지정시 자동 생성
77
+ :return: None
78
+ """
79
+ return self._bootpay.delete(
80
+ f'categories/{category_id}',
81
+ headers=self._supervisor_headers(idempotency_key)
82
+ )
83
+
84
+ def _supervisor_headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
85
+ """
86
+ 카테고리 쓰기(등록/수정/삭제) 요청 헤더 — 서버가 supervisor scope 를 요구한다.
87
+ Idempotency-Key 는 미지정시 매 호출마다 생성된다.
88
+ """
89
+ return {
90
+ 'Idempotency-Key': idempotency_key or str(uuid.uuid4()),
91
+ 'BOOTPAY-ROLE': 'supervisor'
92
+ }
@@ -0,0 +1,51 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+ from urllib.parse import urlencode
3
+
4
+ if TYPE_CHECKING:
5
+ from ..commerce_resource import BootpayCommerceResource
6
+
7
+ from ..types import (
8
+ CommerceCoupon,
9
+ CouponListParams,
10
+ CouponDownloadParams
11
+ )
12
+
13
+
14
+ class CouponModule:
15
+ """쿠폰 모듈"""
16
+
17
+ def __init__(self, bootpay: 'BootpayCommerceResource'):
18
+ self._bootpay = bootpay
19
+
20
+ def list(self, params: Optional[CouponListParams] = None):
21
+ """
22
+ 사용자 보유 쿠폰 목록 조회
23
+ :param params: 조회 파라미터
24
+ :return: List[CommerceCoupon]
25
+ """
26
+ query_params = {}
27
+ if params:
28
+ if params.get('status'):
29
+ query_params['status'] = params['status']
30
+ if params.get('page') is not None:
31
+ query_params['page'] = params['page']
32
+ if params.get('limit') is not None:
33
+ query_params['limit'] = params['limit']
34
+
35
+ query = urlencode(query_params) if query_params else ''
36
+ return self._bootpay.get(f'coupon{"?" + query if query else ""}')
37
+
38
+ def available(self):
39
+ """
40
+ 다운로드 가능한 쿠폰 목록
41
+ :return: List[CommerceCoupon]
42
+ """
43
+ return self._bootpay.get('coupon/available')
44
+
45
+ def download(self, params: CouponDownloadParams):
46
+ """
47
+ 쿠폰 다운로드 (issue_from_template)
48
+ :param params: {'coupon_template_id': str}
49
+ :return: CommerceCoupon
50
+ """
51
+ return self._bootpay.post('coupon/download', params)