producteca 2.0.15__py3-none-any.whl → 2.0.16__py3-none-any.whl

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.
producteca/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ # flake8: noqa
2
+
3
+ from .client import ProductecaClient
@@ -8,4 +8,16 @@ from typing import Optional
8
8
  class BaseService[T](ABC):
9
9
  config: ConfigProducteca
10
10
  endpoint: str
11
- _record: Optional[T] = None
11
+ _record: Optional[T] = None
12
+
13
+ def __repr__(self):
14
+ return repr(self._record)
15
+
16
+ def to_dict(self):
17
+ return self._record.model_dump(by_alias=True)
18
+
19
+ def to_json(self):
20
+ return self._record.model_dump_json(by_alias=True)
21
+
22
+ def __getattr__(self, key):
23
+ return getattr(self._record, key)
producteca/client.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from producteca.config.config import ConfigProducteca
2
2
  from producteca.products.products import ProductService
3
3
  from producteca.sales_orders.sales_orders import SaleOrderService
4
+
4
5
  import os
5
6
 
6
7
 
@@ -20,3 +21,4 @@ class ProductecaClient:
20
21
  @property
21
22
  def SalesOrder(self):
22
23
  return SaleOrderService(self.config)
24
+
@@ -35,7 +35,15 @@ class TestPayments(unittest.TestCase):
35
35
  payment = Payment(**self.payment_data)
36
36
 
37
37
  # Test create method
38
- result = self.client.SalesOrder(id=self.sale_order_id).add_payment(payment)
38
+ result = self.client.SalesOrder(id=self.sale_order_id, invoiceIntegration={
39
+ 'id': 1,
40
+ 'integrationId': 'test-integration',
41
+ 'app': 1,
42
+ 'createdAt': '2023-01-01',
43
+ 'decreaseStock': True,
44
+ "documentUrl": "https://aallala.copm",
45
+ "xmlUrl": "https://aallala.copm",
46
+ }).add_payment(payment.model_dump(by_alias=True))
39
47
 
40
48
  # Assertions
41
49
  mock_post.assert_called_once()
@@ -55,7 +63,15 @@ class TestPayments(unittest.TestCase):
55
63
  payment = Payment(**self.payment_data)
56
64
 
57
65
  # Test update method
58
- result = self.client.SalesOrder(id=self.sale_order_id).update_payment(self.payment_id, payment)
66
+ result = self.client.SalesOrder(id=self.sale_order_id, invoiceIntegration={
67
+ 'id': 1,
68
+ 'integrationId': 'test-integration',
69
+ 'app': 1,
70
+ 'createdAt': '2023-01-01',
71
+ 'decreaseStock': True,
72
+ "documentUrl": "https://aallala.copm",
73
+ "xmlUrl": "https://aallala.copm",
74
+ }).update_payment(self.payment_id, payment.model_dump(by_alias=True))
59
75
 
60
76
  # Assertions
61
77
  mock_put.assert_called_once()
@@ -1,5 +1,5 @@
1
1
  from typing import List, Optional, Union
2
- from pydantic import BaseModel, Field
2
+ from pydantic import BaseModel, Field, ValidationError
3
3
  from dataclasses import dataclass
4
4
  from producteca.abstract.abstract_dataclass import BaseService
5
5
  from producteca.products.search_products import SearchProduct, SearchProductParams
@@ -88,23 +88,71 @@ class MeliCategory(BaseModel):
88
88
  fixed: Optional[bool] = None
89
89
 
90
90
 
91
+ class BundleComponent(BaseModel):
92
+ quantity: int
93
+ variation_id: int = Field(alias='variationId')
94
+ product_id: int = Field(alias='productId')
95
+
96
+
97
+ class BundleVariation(BaseModel):
98
+ variation_id: int = Field(alias='variationId')
99
+ components: List[BundleComponent]
100
+
101
+
102
+ class BundleResult(BaseModel):
103
+ company_id: int = Field(alias='companyId')
104
+ product_id: int = Field(alias='productId')
105
+ variations: List[BundleVariation]
106
+ id: str
107
+
108
+
109
+ class BundleResponse(BaseModel):
110
+ results: List[BundleResult]
111
+ count: int
112
+
113
+
91
114
  class Product(BaseModel):
115
+ integrations: Optional[List[Integration]] = None
116
+ variations: Optional[List[Variation]] = None
117
+ is_simple: Optional[bool] = Field(default=None, alias='isSimple')
118
+ has_variations: Optional[bool] = Field(default=None, alias='hasVariations')
119
+ thumbnail: Optional[str] = None
120
+ category: Optional[str] = None
121
+ notes: Optional[str] = None
122
+ prices: Optional[List[Price]] = None
123
+ buying_price: Optional[float] = Field(default=None, alias='buyingPrice')
124
+ is_archived: Optional[bool] = Field(default=None, alias='isArchived')
125
+ dimensions: Optional[Dimensions] = None
126
+ attributes: Optional[List[Attribute]] = None
127
+ metadata: Optional[List[str]] = None
128
+ is_original: Optional[bool] = Field(default=None, alias='isOriginal')
129
+ name: str
130
+ code: Optional[str] = None
92
131
  sku: Optional[str] = None
93
- variation_id: Optional[int] = None
132
+ brand: Optional[str] = None
133
+ id: Optional[int] = None
134
+
135
+
136
+ class ProductVariationBase(BaseModel):
137
+ sku: str
138
+ variation_id: Optional[int] = Field(default=None, alias='variationId')
94
139
  code: Optional[str] = None
95
- name: Optional[str] = None
96
140
  barcode: Optional[str] = None
97
- attributes: Optional[List[Attribute]] = None
98
- tags: Optional[List[str]] = None
99
- buying_price: Optional[float] = None
100
- dimensions: Optional[Dimensions] = None
101
- category: Optional[Union[str, MeliCategory]]
102
- brand: Optional[str] = None
103
- notes: Optional[str] = None
104
- deals: Optional[List[Deal]] = None
105
- stocks: Optional[List[Stock]] = None
106
- prices: Optional[List[Price]] = None
107
- pictures: Optional[List[Picture]] = None
141
+ attributes: List[Attribute] = []
142
+ tags: Optional[List[str]] = []
143
+ buying_price: Optional[float] = Field(0, alias='buyingPrice')
144
+ dimensions: Optional[Dimensions] = Field(default_factory=Dimensions)
145
+ brand: Optional[str] = ''
146
+ notes: Optional[str] = ''
147
+ deals: Optional[List[Deal]] = []
148
+ stocks: List[Stock]
149
+ prices: Optional[List[Price]] = []
150
+ pictures: Optional[List[Picture]] = []
151
+
152
+
153
+ class ProductVariation(ProductVariationBase):
154
+ category: Optional[str] = Field(default=None)
155
+ name: str
108
156
 
109
157
 
110
158
  class Shipping(BaseModel):
@@ -128,13 +176,14 @@ class AttributeCompletion(BaseModel):
128
176
  total: Optional[int] = None
129
177
 
130
178
 
131
- class MeliProduct(Product):
179
+ class MeliProduct(BaseModel):
132
180
  product_id: Optional[int] = Field(default=None, alias='productId')
181
+ tags: Optional[List[str]] = Field(default=None)
133
182
  has_custom_shipping_costs: Optional[bool] = Field(default=None, alias='hasCustomShippingCosts')
134
183
  shipping: Optional[Shipping] = None
135
184
  mshops_shipping: Optional[MShopsShipping] = Field(default=None, alias='mShopsShipping')
136
185
  add_free_shipping_cost_to_price: Optional[bool] = Field(default=None, alias='addFreeShippingCostToPrice')
137
- category: Optional[Union[str, MeliCategory]] # will never be str, but needs compat with super class
186
+ category: MeliCategory
138
187
  attribute_completion: Optional[AttributeCompletion] = Field(default=None, alias='attributeCompletion')
139
188
  catalog_products: Optional[List[str]] = Field(default=None, alias='catalogProducts')
140
189
  warranty: Optional[str] = None
@@ -143,65 +192,138 @@ class MeliProduct(Product):
143
192
  catalog_products_status: Optional[str] = Field(default=None, alias='catalogProductsStatus')
144
193
 
145
194
 
195
+ class ErrorMessage(BaseModel):
196
+ en: str
197
+ es: str
198
+ pt: str
199
+
200
+
201
+ class ErrorReason(BaseModel):
202
+ code: str
203
+ error: str
204
+ message: ErrorMessage
205
+ data: Optional[dict] = None
206
+
207
+
208
+ class ResolvedValue(BaseModel):
209
+ updated: bool
210
+
211
+
212
+ class ResolvedError(BaseModel):
213
+ resolved: Optional[bool] = None
214
+ reason: Optional[ErrorReason] = None
215
+ value: Optional[ResolvedValue] = None
216
+ statusCode: Optional[int] = None
217
+
218
+
219
+ class ErrorContext(BaseModel):
220
+ _ns_name: str
221
+ id: int
222
+ requestId: str
223
+ tokenAppId: str
224
+ appId: str
225
+ bearer: str
226
+ eventId: str
227
+
228
+
229
+ class SynchronizeResponse(BaseModel):
230
+ product: Optional[ResolvedError] = None
231
+ variation: Optional[ResolvedError] = None
232
+ deals: Optional[ResolvedError] = None
233
+ bundles: Optional[ResolvedError] = None
234
+ taxes: Optional[ResolvedError] = None
235
+ meliProductListingIntegrations: Optional[ResolvedError] = None
236
+ tags: Optional[ResolvedError] = None
237
+ productIntegrations: Optional[ResolvedError] = None
238
+ statusCode: Optional[int] = None
239
+ error_context: Optional[ErrorContext] = Field(None, alias='error@context')
240
+
241
+
242
+ class ListedSynchronizeResponse(BaseModel):
243
+ results: List[SynchronizeResponse]
244
+
245
+
146
246
  @dataclass
147
247
  class ProductService(BaseService[Product]):
148
- endpoint: str = Field(default='products', exclude=True)
248
+ endpoint: str = 'products'
149
249
  create_if_it_doesnt_exist: bool = Field(default=False, exclude=True)
150
250
 
151
251
  def __call__(self, **payload):
152
252
  self._record = Product(**payload)
153
253
  return self
154
254
 
155
- def create(self):
255
+ def synchronize(self, payload) -> Union[Product, SynchronizeResponse]:
256
+
156
257
  endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
157
258
  headers = self.config.headers.copy()
158
259
  headers.update({"createifitdoesntexist": str(self.create_if_it_doesnt_exist).lower()})
159
- data = self._record.model_dump_json(by_alias=True, exclude_none=True)
160
- response = requests.post(endpoint_url, data=data, headers=headers)
161
- if response.status_code == 204:
162
- raise Exception("Product does not exist and the request cant create if it does not exist")
163
- return Product(**response.json())
260
+ product_variation = ProductVariation(**payload)
261
+ if not product_variation.code and not product_variation.sku:
262
+ raise Exception("Sku or code should be provided to update the product")
263
+ data = product_variation.model_dump(by_alias=True, exclude_none=True)
264
+ response = requests.post(endpoint_url, json=data, headers=headers)
265
+ response_data = response.json()
266
+ try:
267
+ return Product(**response_data)
268
+ except ValidationError:
269
+ pass
270
+ if isinstance(response_data, list):
271
+ res = ListedSynchronizeResponse(results=response_data)
272
+ if any([r.error_context for r in res.results]):
273
+ raise Exception(f"Errored while updating {res.results[0].error_context} {res.model_dump_json()}")
274
+ else:
275
+ return res.results[0]
276
+ else:
277
+ try:
278
+ sync_resp = SynchronizeResponse(**response_data)
279
+ if sync_resp.error_context:
280
+ raise Exception(f"Errored while updating {sync_resp.error_context} - {sync_resp.model_dump_json()}")
281
+ else:
282
+ return sync_resp
283
+ except ValidationError:
284
+ try:
285
+ error_res = ErrorReason(**response_data)
286
+ raise Exception(f"Errored with the following message {error_res.message} - {error_res.model_dump_json()}")
287
+ except ValidationError:
288
+ pass
164
289
 
165
- def update(self):
166
- # TODO: Change name to synchronize
167
- endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
168
- headers = self.config.headers.copy()
169
- data = self._record.model_dump_json(by_alias=True, exclude_none=True)
170
- if not self._record.code and not self._record.sku:
171
- raise "Sku or code should be provided to update the product"
172
- response = requests.post(endpoint_url, data=data, headers=headers)
290
+ if not response.ok:
291
+ raise Exception(f"Error getting product {product_variation.sku} - {product_variation.code}\n {response.text}")
173
292
  if response.status_code == 204:
174
- raise Exception("Product does not exist and the request cant create if it does not exist")
175
- return Product(**response.json())
293
+ raise Exception("Status code is 204, meaning nothing was updated or created")
294
+ raise Exception(f"Unhandled error, check response {response.text}")
176
295
 
177
- def get(self, product_id: int) -> "Product":
296
+ def get(self, product_id: int) -> "ProductService":
178
297
  endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}')
179
298
  headers = self.config.headers
180
299
  response = requests.get(endpoint_url, headers=headers)
181
300
  if not response.ok:
182
301
  raise Exception(f"Error getting product {product_id}\n {response.text}")
183
302
  response_data = response.json()
184
- return Product(**response_data)
303
+ return self(**response_data)
185
304
 
186
- def get_bundle(self, product_id: int) -> "Product":
305
+ def get_bundle(self, product_id: int) -> BundleResponse:
187
306
  endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/bundles')
188
307
  headers = self.config.headers
189
308
  response = requests.get(endpoint_url, headers=headers)
190
309
  if not response.ok:
191
310
  raise Exception(f"Error getting bundle {product_id}\n {response.text}")
192
- return Product(**response.json())
311
+ return BundleResponse(**response.json())
193
312
 
194
- def get_ml_integration(self, product_id: int) -> "MeliProduct":
313
+ def get_ml_integration(self, product_id: int) -> MeliProduct:
195
314
  endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/listingintegration')
196
315
  headers = self.config.headers
197
316
  response = requests.get(endpoint_url, headers=headers)
198
317
  if not response.ok:
199
318
  raise Exception(f"Error getting ml integration {product_id}\n {response.text}")
200
- return MeliProduct(**response.json())
319
+ response_data = response.json()
320
+ return MeliProduct(**response_data)
201
321
 
202
322
  def search(self, params: SearchProductParams) -> SearchProduct:
203
323
  endpoint: str = f'search/{self.endpoint}'
204
324
  headers = self.config.headers
205
325
  url = self.config.get_endpoint(endpoint)
206
326
  response = requests.get(url, headers=headers, params=params.model_dump(by_alias=True, exclude_none=True))
327
+ if not response.ok:
328
+ raise Exception(f"error in searching products {response.text}")
207
329
  return SearchProduct(**response.json())
@@ -1,17 +1,17 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
  from pydantic import BaseModel, Field
3
3
 
4
4
 
5
5
  class FacetValue(BaseModel):
6
6
  count: int
7
- value: str
8
- label: str
7
+ value: Optional[Union[str, bool]] = None
8
+ label: Union[str, bool]
9
9
 
10
10
 
11
11
  class Facet(BaseModel):
12
12
  key: str
13
13
  value: List[FacetValue]
14
- is_collection: bool
14
+ is_collection: Optional[bool] = False
15
15
  translate: bool
16
16
 
17
17
 
@@ -22,23 +22,23 @@ class SearchStocks(BaseModel):
22
22
 
23
23
 
24
24
  class SearchPrices(BaseModel):
25
- price_list_id: int
26
- price_list: str
25
+ price_list_id: int = Field(..., alias='priceListId')
26
+ price_list: str = Field(..., alias='priceList')
27
27
  amount: float
28
28
  currency: str
29
29
 
30
30
 
31
31
  class SearchIntegration(BaseModel):
32
- app: Optional[int]
33
- integration_id: Optional[str]
34
- permalink: Optional[str]
35
- status: Optional[str]
36
- listing_type: Optional[str]
37
- safety_stock: Optional[int]
38
- synchronize_stock: Optional[bool]
39
- is_active: Optional[bool]
40
- is_active_or_paused: Optional[bool]
41
- id: Optional[int]
32
+ app: Optional[int] = None
33
+ integration_id: Optional[str] = Field(None, alias='integrationId')
34
+ permalink: Optional[str] = None
35
+ status: Optional[str] = None
36
+ listing_type: Optional[str] = Field(None, alias='listingType')
37
+ safety_stock: Optional[int] = Field(None, alias='safetyStock')
38
+ synchronize_stock: Optional[bool] = Field(None, alias='synchronizeStock')
39
+ is_active: Optional[bool] = Field(None, alias='isActive')
40
+ is_active_or_paused: Optional[bool] = Field(None, alias='isActiveOrPaused')
41
+ id: Optional[int] = None
42
42
 
43
43
 
44
44
  class SearchDeals(BaseModel):
@@ -56,8 +56,8 @@ class SearchDeals(BaseModel):
56
56
  class SearchResultItem(BaseModel):
57
57
  search_score: float = Field(..., alias='@search.score')
58
58
  id: int
59
- product_id: int
60
- company_id: int
59
+ product_id: int = Field(..., alias='productId')
60
+ company_id: int = Field(..., alias='companyId')
61
61
  name: str
62
62
  code: str
63
63
  skus: List[str]
@@ -65,61 +65,61 @@ class SearchResultItem(BaseModel):
65
65
  category: str
66
66
  thumbnail: str
67
67
  stocks: List[SearchStocks]
68
- warehouses_with_stock: List[str]
69
- total_stock: int
70
- has_pictures: bool
71
- buying_price: float
68
+ warehouses_with_stock: List[str] = Field(..., alias='warehousesWithStock')
69
+ total_stock: int = Field(..., alias='totalStock')
70
+ has_pictures: bool = Field(..., alias='hasPictures')
71
+ buying_price: float = Field(..., alias='buyingPrice')
72
72
  prices: List[SearchPrices]
73
- integration_ids: List[str]
74
- integration_apps: List[str]
73
+ integration_ids: List[str] = Field(..., alias='integrationIds')
74
+ integration_apps: List[str] = Field(..., alias='integrationApps')
75
75
  integrations: List[SearchIntegration]
76
76
  campaigns: List[str]
77
- app: Optional[int]
78
- status: Optional[str]
79
- synchronize_stock: Optional[bool]
80
- listing_type: Optional[str]
81
- price_amount: Optional[float]
82
- price_currency: Optional[str]
83
- category_id: Optional[str]
84
- category_base_id: Optional[str]
85
- category_l1: Optional[str]
86
- category_l2: Optional[str]
87
- category_l3: Optional[str]
88
- category_l4: Optional[str]
89
- category_l5: Optional[str]
90
- category_l6: Optional[str]
91
- has_category: Optional[bool]
92
- category_fixed: Optional[bool]
93
- accepts_mercadoenvios: Optional[bool]
94
- shipping_mode: Optional[str]
95
- local_pickup: Optional[bool]
96
- mandatory_free_shipping: Optional[bool]
97
- free_shipping: Optional[bool]
98
- free_shipping_cost: Optional[float]
99
- template: Optional[str]
100
- youtube_id: Optional[str]
101
- warranty: Optional[str]
102
- permalink: Optional[str]
103
- domain: Optional[str]
104
- attribute_completion_status: Optional[str]
105
- attribute_completion_count: Optional[int]
106
- attribute_completion_total: Optional[int]
107
- deals: Optional[SearchDeals]
108
- campaign_status: Optional[List[str]]
109
- size_chart: Optional[str]
110
- channel_status: Optional[List[str]]
111
- channel_category_l1: Optional[List[str]]
112
- channel_category_l2: Optional[List[str]]
113
- channel_category_l3: Optional[List[str]]
114
- channel_category_id: Optional[List[str]]
115
- channel_synchronizes_stock: Optional[List[str]]
116
- channel_has_category: Optional[List[str]]
117
- catalog_products_status: Optional[List[str]]
118
- metadata: Optional[List[str]]
119
- integration_tags: Optional[List[str]]
120
- variations_integration_ids: Optional[List[str]]
121
- channel_pictures_templates: Optional[List[str]]
122
- channel_pictures_templates_apps: Optional[List[str]]
77
+ app: Optional[int] = None
78
+ status: Optional[str] = None
79
+ synchronize_stock: Optional[bool] = Field(None, alias='synchronizeStock')
80
+ listing_type: Optional[str] = Field(None, alias='listingType')
81
+ price_amount: Optional[float] = Field(None, alias='priceAmount')
82
+ price_currency: Optional[str] = Field(None, alias='priceCurrency')
83
+ category_id: Optional[str] = Field(None, alias='categoryId')
84
+ category_base_id: Optional[str] = Field(None, alias='categoryBaseId')
85
+ category_l1: Optional[str] = Field(None, alias='categoryL1')
86
+ category_l2: Optional[str] = Field(None, alias='categoryL2')
87
+ category_l3: Optional[str] = Field(None, alias='categoryL3')
88
+ category_l4: Optional[str] = Field(None, alias='categoryL4')
89
+ category_l5: Optional[str] = Field(None, alias='categoryL5')
90
+ category_l6: Optional[str] = Field(None, alias='categoryL6')
91
+ has_category: Optional[bool] = Field(None, alias='hasCategory')
92
+ category_fixed: Optional[bool] = Field(None, alias='categoryFixed')
93
+ accepts_mercadoenvios: Optional[bool] = Field(None, alias='acceptsMercadoenvios')
94
+ shipping_mode: Optional[str] = Field(None, alias='shippingMode')
95
+ local_pickup: Optional[bool] = Field(None, alias='localPickup')
96
+ mandatory_free_shipping: Optional[bool] = Field(None, alias='mandatoryFreeShipping')
97
+ free_shipping: Optional[bool] = Field(None, alias='freeShipping')
98
+ free_shipping_cost: Optional[float] = Field(None, alias='freeShippingCost')
99
+ template: Optional[str] = None
100
+ youtube_id: Optional[str] = Field(None, alias='youtubeId')
101
+ warranty: Optional[str] = None
102
+ permalink: Optional[str] = None
103
+ domain: Optional[str] = None
104
+ attribute_completion_status: Optional[str] = Field(None, alias='attributeCompletionStatus')
105
+ attribute_completion_count: Optional[int] = Field(None, alias='attributeCompletionCount')
106
+ attribute_completion_total: Optional[int] = Field(None, alias='attributeCompletionTotal')
107
+ deals: Optional[SearchDeals] = None
108
+ campaign_status: Optional[List[str]] = Field(None, alias='campaignStatus')
109
+ size_chart: Optional[str] = Field(None, alias='sizeChart')
110
+ channel_status: Optional[List[str]] = Field(None, alias='channelStatus')
111
+ channel_category_l1: Optional[List[str]] = Field(None, alias='channelCategoryL1')
112
+ channel_category_l2: Optional[List[str]] = Field(None, alias='channelCategoryL2')
113
+ channel_category_l3: Optional[List[str]] = Field(None, alias='channelCategoryL3')
114
+ channel_category_id: Optional[List[str]] = Field(None, alias='channelCategoryId')
115
+ channel_synchronizes_stock: Optional[List[str]] = Field(None, alias='channelSynchronizesStock')
116
+ channel_has_category: Optional[List[str]] = Field(None, alias='channelHasCategory')
117
+ catalog_products_status: Optional[List[str]] = Field(None, alias='catalogProductsStatus')
118
+ metadata: Optional[List[str]] = None
119
+ integration_tags: Optional[List[str]] = Field(None, alias='integrationTags')
120
+ variations_integration_ids: Optional[List[str]] = Field(None, alias='variationsIntegrationIds')
121
+ channel_pictures_templates: Optional[List[str]] = Field(None, alias='channelPicturesTemplates')
122
+ channel_pictures_templates_apps: Optional[List[str]] = Field(None, alias='channelPicturesTemplatesApps')
123
123
 
124
124
 
125
125
  class SearchProduct(BaseModel):
@@ -132,8 +132,5 @@ class SearchProductParams(BaseModel):
132
132
  top: Optional[int]
133
133
  skip: Optional[int]
134
134
  filter: Optional[str] = Field(default=None, alias='$filter')
135
- search: Optional[str]
135
+ search: Optional[str] = Field(default=None)
136
136
  sales_channel: Optional[str] = Field(default='2', alias='salesChannel')
137
-
138
-
139
-