bootpay-backend 2.3.0__tar.gz → 2.7.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 (68) hide show
  1. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/PKG-INFO +4 -3
  2. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/README.md +3 -2
  3. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/__init__.py +24 -2
  4. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/commerce_resource.py +53 -26
  5. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/__init__.py +5 -1
  6. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/category.py +92 -0
  7. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/invoice.py +106 -0
  8. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/mall_setting.py +63 -0
  9. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/order.py +6 -1
  10. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/order_cancel.py +139 -0
  11. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/order_subscription.py +390 -0
  12. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/order_subscription_adjustment.py +102 -0
  13. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/order_subscription_bill.py +32 -16
  14. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/order_subscription_request.py +131 -0
  15. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/product.py +235 -0
  16. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/store.py +45 -0
  17. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/user.py +238 -0
  18. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/user_group.py +55 -9
  19. bootpay_backend-2.7.0/bootpay_backend/commerce/modules/webhook.py +34 -0
  20. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/types/__init__.py +482 -2
  21. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/rest_client.py +63 -2
  22. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend.egg-info/PKG-INFO +4 -3
  23. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend.egg-info/SOURCES.txt +10 -0
  24. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/setup.py +1 -1
  25. bootpay_backend-2.7.0/tests/commerce/test_auth_contract.py +86 -0
  26. bootpay_backend-2.7.0/tests/commerce/test_mall_setting.py +20 -0
  27. bootpay_backend-2.7.0/tests/commerce/test_order_subscription_charge.py +50 -0
  28. bootpay_backend-2.7.0/tests/commerce/test_product_mall.py +27 -0
  29. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/commerce/test_token.py +7 -0
  30. bootpay_backend-2.7.0/tests/commerce/test_user_mall.py +44 -0
  31. bootpay_backend-2.7.0/tests/commerce/test_webhook.py +20 -0
  32. bootpay_backend-2.7.0/tests/commerce/test_wire_format.py +728 -0
  33. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/conftest.py +13 -0
  34. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_legacy_compatibility.py +59 -0
  35. bootpay_backend-2.7.0/tests/pg/test_sequential_billing_key.py +29 -0
  36. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_token.py +7 -0
  37. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/category.py +0 -60
  38. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/invoice.py +0 -61
  39. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/order_cancel.py +0 -77
  40. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/order_subscription.py +0 -217
  41. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/order_subscription_adjustment.py +0 -52
  42. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/order_subscription_request.py +0 -92
  43. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/product.py +0 -93
  44. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/store.py +0 -25
  45. bootpay_backend-2.3.0/bootpay_backend/commerce/modules/user.py +0 -114
  46. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/LICENSE +0 -0
  47. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/__init__.py +0 -0
  48. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/bankcode.py +0 -0
  49. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/cart.py +0 -0
  50. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/coupon.py +0 -0
  51. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend/commerce/modules/point.py +0 -0
  52. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend.egg-info/dependency_links.txt +0 -0
  53. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend.egg-info/not-zip-safe +0 -0
  54. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend.egg-info/requires.txt +0 -0
  55. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/bootpay_backend.egg-info/top_level.txt +0 -0
  56. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/setup.cfg +0 -0
  57. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/__init__.py +0 -0
  58. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/commerce/__init__.py +0 -0
  59. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/commerce/test_order.py +0 -0
  60. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/commerce/test_product.py +0 -0
  61. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/commerce/test_store.py +0 -0
  62. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/commerce/test_user.py +0 -0
  63. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/__init__.py +0 -0
  64. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_auth.py +0 -0
  65. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_billing.py +0 -0
  66. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_cash_receipt.py +0 -0
  67. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_payment.py +0 -0
  68. {bootpay_backend-2.3.0 → bootpay_backend-2.7.0}/tests/pg/test_wallet.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bootpay_backend
3
- Version: 2.3.0
3
+ Version: 2.7.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>
@@ -444,11 +444,12 @@ if 'error_code' not in token:
444
444
 
445
445
  ## Documentation
446
446
 
447
- [부트페이 개발매뉴얼](https://developer.bootpay.co.kr/)을 참조해주세요
447
+ - [부트페이 결제 개발문서](https://developers.bootpay.ai)
448
+ - [부트페이 커머스 개발문서](https://commerce.bootpay.ai)
448
449
 
449
450
  ## 기술문의
450
451
 
451
- [부트페이 홈페이지](https://www.bootpay.co.kr) 우측 하단 채팅을 통해 기술문의 주세요!
452
+ [부트페이 홈페이지](https://www.bootpay.ai)에서 문의해주세요!
452
453
 
453
454
  ## License
454
455
 
@@ -404,11 +404,12 @@ if 'error_code' not in token:
404
404
 
405
405
  ## Documentation
406
406
 
407
- [부트페이 개발매뉴얼](https://developer.bootpay.co.kr/)을 참조해주세요
407
+ - [부트페이 결제 개발문서](https://developers.bootpay.ai)
408
+ - [부트페이 커머스 개발문서](https://commerce.bootpay.ai)
408
409
 
409
410
  ## 기술문의
410
411
 
411
- [부트페이 홈페이지](https://www.bootpay.co.kr) 우측 하단 채팅을 통해 기술문의 주세요!
412
+ [부트페이 홈페이지](https://www.bootpay.ai)에서 문의해주세요!
412
413
 
413
414
  ## License
414
415
 
@@ -15,7 +15,9 @@ from .modules import (
15
15
  CouponModule,
16
16
  PointModule,
17
17
  CartModule,
18
- OrderSubscriptionRequestModule
18
+ OrderSubscriptionRequestModule,
19
+ MallSettingModule,
20
+ WebhookModule
19
21
  )
20
22
  from .types import *
21
23
 
@@ -55,7 +57,7 @@ class BootpayCommerce(BootpayCommerceResource):
55
57
  :param mode: 환경 ('development', 'stage', 'production')
56
58
  """
57
59
  super().__init__()
58
- if client_key and secret_key:
60
+ if client_key is not None or secret_key is not None:
59
61
  self.set_configuration(client_key, secret_key, mode)
60
62
  self._init_modules()
61
63
 
@@ -76,6 +78,8 @@ class BootpayCommerce(BootpayCommerceResource):
76
78
  self.point = PointModule(self)
77
79
  self.cart = CartModule(self)
78
80
  self.order_subscription_request = OrderSubscriptionRequestModule(self)
81
+ self.mall_setting = MallSettingModule(self)
82
+ self.webhook = WebhookModule(self)
79
83
 
80
84
  def get_access_token(self):
81
85
  """
@@ -206,6 +210,8 @@ __all__ = [
206
210
  'PointModule',
207
211
  'CartModule',
208
212
  'OrderSubscriptionRequestModule',
213
+ 'MallSettingModule',
214
+ 'WebhookModule',
209
215
  # Types
210
216
  'ListParams',
211
217
  'CommerceAddress',
@@ -287,4 +293,20 @@ __all__ = [
287
293
  'OrderSubscriptionRequest',
288
294
  'OrderSubscriptionRequestListParams',
289
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',
290
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',
@@ -77,24 +86,26 @@ class BootpayCommerceResource:
77
86
  if include_auth:
78
87
  basic_auth = self._get_basic_auth_header()
79
88
  if basic_auth:
80
- headers['Authorization'] = basic_auth
81
- return headers
89
+ merged['Authorization'] = basic_auth
90
+ if headers:
91
+ merged.update(headers)
92
+ return merged
82
93
 
83
- def get(self, url: str, params: Optional[Dict] = None):
94
+ def get(self, url: str, params: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
84
95
  """GET 요청"""
85
96
  response = requests.get(
86
97
  self._entrypoints(url),
87
- headers=self._get_headers(),
98
+ headers=self._get_headers(headers=headers),
88
99
  params=params,
89
100
  timeout=self.timeout
90
101
  )
91
102
  return response.json()
92
103
 
93
- 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):
94
105
  """POST 요청"""
95
106
  response = requests.post(
96
107
  self._entrypoints(url),
97
- headers=self._get_headers(),
108
+ headers=self._get_headers(headers=headers),
98
109
  json=data,
99
110
  timeout=self.timeout
100
111
  )
@@ -112,14 +123,18 @@ class BootpayCommerceResource:
112
123
  )
113
124
  return response.json()
114
125
 
115
- 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):
116
128
  """
117
129
  Multipart/form-data POST 요청 (이미지 업로드 포함)
130
+ ⚠️ Content-Type 은 requests 가 생성한 값(boundary 포함)을 그대로 사용한다.
131
+ 직접 지정하면 boundary 가 사라져 본문이 서버에서 null 로 파싱된다.
118
132
  :param url: 요청 URL
119
133
  :param data: 폼 데이터
120
134
  :param image_paths: 이미지 파일 경로 배열
135
+ :param headers: 요청별 추가 헤더 (공통값을 덮어쓴다)
121
136
  """
122
- headers = {
137
+ request_headers = {
123
138
  'Accept': 'application/json',
124
139
  'Accept-Charset': 'utf-8',
125
140
  'BOOTPAY-SDK-VERSION': self.SDK_VERSION,
@@ -129,28 +144,35 @@ class BootpayCommerceResource:
129
144
  }
130
145
  basic_auth = self._get_basic_auth_header()
131
146
  if basic_auth:
132
- headers['Authorization'] = basic_auth
147
+ request_headers['Authorization'] = basic_auth
148
+ if headers:
149
+ request_headers.update(headers)
133
150
 
134
151
  # 폼 데이터 준비
152
+ # ⚠️ bool 은 소문자 'true'/'false' 로 보낸다 — str(True) 의 'True'/'False' 는
153
+ # Rails 캐스팅 대상이 아니라서 "False" 가 true 로 해석되는 실위험이 있다.
135
154
  form_data = {}
136
155
  for key, value in data.items():
137
156
  if value is not None:
138
- 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)):
139
160
  form_data[key] = json.dumps(value)
140
161
  else:
141
162
  form_data[key] = str(value)
142
163
 
143
164
  # 파일 준비
165
+ # ⚠️ Rails 는 반복된 `images` 를 배열로 받지 않는다. images[0], images[1] ... 로 인덱싱해야 한다.
144
166
  files = []
145
167
  if image_paths:
146
- for image_path in image_paths:
168
+ for index, image_path in enumerate(image_paths):
147
169
  filename = os.path.basename(image_path)
148
- files.append(('images', (filename, open(image_path, 'rb'))))
170
+ files.append((f'images[{index}]', (filename, open(image_path, 'rb'))))
149
171
 
150
172
  try:
151
173
  response = requests.post(
152
174
  self._entrypoints(url),
153
- headers=headers,
175
+ headers=request_headers,
154
176
  data=form_data,
155
177
  files=files if files else None,
156
178
  timeout=self.timeout
@@ -161,22 +183,27 @@ class BootpayCommerceResource:
161
183
  for _, file_tuple in files:
162
184
  file_tuple[1].close()
163
185
 
164
- 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):
165
187
  """PUT 요청"""
166
188
  response = requests.put(
167
189
  self._entrypoints(url),
168
- headers=self._get_headers(),
190
+ headers=self._get_headers(headers=headers),
169
191
  json=data,
170
192
  timeout=self.timeout
171
193
  )
172
194
  return response.json()
173
195
 
174
- def delete(self, url: str, params: Optional[Dict] = None):
175
- """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
+ """
176
202
  response = requests.delete(
177
203
  self._entrypoints(url),
178
- headers=self._get_headers(),
204
+ headers=self._get_headers(headers=headers),
179
205
  params=params,
206
+ json=data,
180
207
  timeout=self.timeout
181
208
  )
182
209
  return response.json()
@@ -14,6 +14,8 @@ from .coupon import CouponModule
14
14
  from .point import PointModule
15
15
  from .cart import CartModule
16
16
  from .order_subscription_request import OrderSubscriptionRequestModule
17
+ from .mall_setting import MallSettingModule
18
+ from .webhook import WebhookModule
17
19
 
18
20
  __all__ = [
19
21
  'UserModule',
@@ -30,5 +32,7 @@ __all__ = [
30
32
  'CouponModule',
31
33
  'PointModule',
32
34
  'CartModule',
33
- 'OrderSubscriptionRequestModule'
35
+ 'OrderSubscriptionRequestModule',
36
+ 'MallSettingModule',
37
+ 'WebhookModule'
34
38
  ]
@@ -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,106 @@
1
+ import uuid
2
+ from typing import TYPE_CHECKING, Optional, List, Dict
3
+ from urllib.parse import urlencode
4
+
5
+ if TYPE_CHECKING:
6
+ from ..commerce_resource import BootpayCommerceResource
7
+
8
+ from ..types import (
9
+ CommerceInvoice,
10
+ InvoiceListParams,
11
+ InvoiceListResponse,
12
+ ListParams
13
+ )
14
+
15
+
16
+ class InvoiceModule:
17
+ """청구서 모듈"""
18
+
19
+ def __init__(self, bootpay: 'BootpayCommerceResource'):
20
+ self._bootpay = bootpay
21
+
22
+ def list(self, params: Optional[InvoiceListParams] = None):
23
+ """
24
+ 청구서 목록 조회
25
+ GET /v1/invoices
26
+ 응답은 {'list': [...], 'count': N} 구조다 ({'items', 'total'} 아님).
27
+ limit 미지정시 서버 기본값과 동일한 24 를 보낸다.
28
+ :param params: 조회 파라미터
29
+ :return: InvoiceListResponse — {'list': List[CommerceInvoice], 'count': int}
30
+ """
31
+ params = dict(params or {})
32
+ idempotency_key = params.pop('idempotency_key', None)
33
+
34
+ query_params = {
35
+ 'page': 1 if params.get('page') is None else params['page'],
36
+ 'limit': 24 if params.get('limit') is None else params['limit'],
37
+ }
38
+ if params.get('keyword'):
39
+ query_params['keyword'] = params['keyword']
40
+ if params.get('cs_type'):
41
+ query_params['cs_type'] = params['cs_type']
42
+ if params.get('user_id'):
43
+ query_params['user_id'] = params['user_id']
44
+ if params.get('product_type') is not None:
45
+ query_params['product_type'] = params['product_type']
46
+ if params.get('css_at'):
47
+ query_params['css_at'] = params['css_at']
48
+ if params.get('cse_at'):
49
+ query_params['cse_at'] = params['cse_at']
50
+
51
+ return self._bootpay.get(
52
+ f'invoices?{urlencode(query_params)}',
53
+ headers=self._invoice_headers(idempotency_key)
54
+ )
55
+
56
+ def create(self, invoice: CommerceInvoice):
57
+ """
58
+ 청구서 생성
59
+ :param invoice: 청구서 정보
60
+ :return: CommerceInvoice
61
+ """
62
+ return self._bootpay.post('invoices', invoice)
63
+
64
+ def notify(self, invoice_id: str, send_types: Optional[List[int]] = None,
65
+ idempotency_key: Optional[str] = None):
66
+ """
67
+ 청구서 알림 재발송
68
+ POST /v1/invoices/{invoice_id}/notify
69
+ send_types 미전달시 서버가 빈 배열로 처리한다.
70
+ ⚠️ 실제 고객에게 알림이 발송되므로 테스트 호출 주의.
71
+ :param invoice_id: 청구서 ID
72
+ :param send_types: 발송 타입 배열 (예: [1, 2] - SMS, Email 등)
73
+ :param idempotency_key: 미지정시 자동 생성
74
+ :return: None
75
+ """
76
+ payload = {}
77
+ if send_types is not None:
78
+ payload['send_types'] = send_types
79
+ return self._bootpay.post(
80
+ f'invoices/{invoice_id}/notify',
81
+ payload,
82
+ headers=self._invoice_headers(idempotency_key)
83
+ )
84
+
85
+ def detail(self, invoice_id: str, idempotency_key: Optional[str] = None):
86
+ """
87
+ 청구서 상세 조회
88
+ GET /v1/invoices/{invoice_id}
89
+ :param invoice_id: 청구서 ID
90
+ :param idempotency_key: 미지정시 자동 생성
91
+ :return: CommerceInvoice
92
+ """
93
+ return self._bootpay.get(
94
+ f'invoices/{invoice_id}',
95
+ headers=self._invoice_headers(idempotency_key)
96
+ )
97
+
98
+ def _invoice_headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
99
+ """
100
+ 청구서 API 요청 헤더
101
+ Idempotency-Key 는 미지정시 매 호출마다 생성된다.
102
+ """
103
+ return {
104
+ 'Idempotency-Key': idempotency_key or str(uuid.uuid4()),
105
+ 'BOOTPAY-ROLE': 'user'
106
+ }
@@ -0,0 +1,63 @@
1
+ import uuid
2
+ from typing import TYPE_CHECKING, Optional, Dict, Any
3
+
4
+ if TYPE_CHECKING:
5
+ from ..commerce_resource import BootpayCommerceResource
6
+
7
+ from ..types import (
8
+ CommerceMallSetting,
9
+ MallSettingUpdateParams
10
+ )
11
+
12
+
13
+ class MallSettingModule:
14
+ """몰 설정 모듈 (supervisor scope 전용)"""
15
+
16
+ def __init__(self, bootpay: 'BootpayCommerceResource'):
17
+ self._bootpay = bootpay
18
+
19
+ def get_mall_setting(self, idempotency_key: Optional[str] = None):
20
+ """
21
+ 몰 설정 조회
22
+ GET /v1/mall-setting (supervisor scope 토큰 전용)
23
+ :param idempotency_key: 미지정시 자동 생성
24
+ :return: CommerceMallSetting
25
+ """
26
+ return self._bootpay.get('mall-setting', headers=self._supervisor_headers(idempotency_key))
27
+
28
+ def detail(self, idempotency_key: Optional[str] = None):
29
+ """get_mall_setting 별칭"""
30
+ return self.get_mall_setting(idempotency_key)
31
+
32
+ def update_mall_setting(self, params: MallSettingUpdateParams, idempotency_key: Optional[str] = None):
33
+ """
34
+ 몰 설정 수정
35
+ PUT /v1/mall-setting (supervisor scope 토큰 전용)
36
+ 요청 바디는 flatten 형식이며 전달된 값(non-None)만 서버로 전송된다.
37
+ :param params: 수정할 설정값
38
+ :param idempotency_key: 미지정시 자동 생성
39
+ :return: CommerceMallSetting
40
+ """
41
+ return self._bootpay.put(
42
+ 'mall-setting',
43
+ self._compact(params),
44
+ headers=self._supervisor_headers(idempotency_key)
45
+ )
46
+
47
+ def update(self, params: MallSettingUpdateParams, idempotency_key: Optional[str] = None):
48
+ """update_mall_setting 별칭"""
49
+ return self.update_mall_setting(params, idempotency_key)
50
+
51
+ def _compact(self, params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
52
+ """None 값을 제거한다."""
53
+ return {k: v for k, v in (params or {}).items() if v is not None}
54
+
55
+ def _supervisor_headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
56
+ """
57
+ supervisor 전용 요청 헤더
58
+ Idempotency-Key 는 미지정시 매 호출마다 생성된다.
59
+ """
60
+ return {
61
+ 'Idempotency-Key': idempotency_key or str(uuid.uuid4()),
62
+ 'BOOTPAY-ROLE': 'supervisor'
63
+ }
@@ -36,6 +36,11 @@ class OrderModule:
36
36
  query_params['user_group_id'] = params['user_group_id']
37
37
  if params.get('cs_type'):
38
38
  query_params['cs_type'] = params['cs_type']
39
+ if params.get('search_date_from'):
40
+ query_params['search_date_from'] = params['search_date_from']
41
+ if params.get('search_date_to'):
42
+ query_params['search_date_to'] = params['search_date_to']
43
+ # css_at / cse_at 는 서버 별칭으로 계속 지원 (정식 키는 search_date_from / search_date_to)
39
44
  if params.get('css_at'):
40
45
  query_params['css_at'] = params['css_at']
41
46
  if params.get('cse_at'):
@@ -47,7 +52,7 @@ class OrderModule:
47
52
  if params.get('payment_status') and len(params['payment_status']) > 0:
48
53
  query_params['payment_status'] = ','.join(map(str, params['payment_status']))
49
54
  if params.get('order_subscription_ids') and len(params['order_subscription_ids']) > 0:
50
- query_params['order_subscription_ids'] = ','.join(params['order_subscription_ids'])
55
+ query_params['order_subscription_ids'] = ','.join(map(str, params['order_subscription_ids']))
51
56
 
52
57
  query = urlencode(query_params) if query_params else ''
53
58
  return self._bootpay.get(f'orders{"?" + query if query else ""}')