diff --git a/connect_ext_ppr/client/client.py b/connect_ext_ppr/client/client.py
index 13010e6..9bcf69a 100644
--- a/connect_ext_ppr/client/client.py
+++ b/connect_ext_ppr/client/client.py
@@ -10,7 +10,7 @@
from requests_oauthlib import OAuth1
from connect_ext_ppr.client.auth import APSTokenAuth
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
from connect_ext_ppr.client.ns import (
Collection,
Resource,
@@ -88,13 +88,13 @@ def execute_request(
except Exception as e:
if response is not None:
- raise ClientError(
+ raise CBCClientError(
message=f'{type(e).__name__} : {str(e)}',
response=response,
cause=e,
)
else:
- raise ClientError(
+ raise CBCClientError(
message=f'{type(e).__name__} : {str(e)}',
cause=e,
)
diff --git a/connect_ext_ppr/client/exception.py b/connect_ext_ppr/client/exception.py
index a7aa2ed..95566b3 100644
--- a/connect_ext_ppr/client/exception.py
+++ b/connect_ext_ppr/client/exception.py
@@ -3,7 +3,7 @@
from requests import Response
-class ClientError(RuntimeError):
+class CBCClientError(RuntimeError):
def __init__(
self,
message: str,
diff --git a/connect_ext_ppr/client/utils.py b/connect_ext_ppr/client/utils.py
new file mode 100644
index 0000000..8bfb536
--- /dev/null
+++ b/connect_ext_ppr/client/utils.py
@@ -0,0 +1,11 @@
+from connect_ext_ppr.services.cbc_extension import get_hub_credentials
+from connect_ext_ppr.services.cbc_hub import CBCService
+from connect_ext_ppr.errors import ExtensionHttpError
+
+
+def get_cbc_service(hub_id, cbc_db, verify_certificate=False):
+ hub_credentials = get_hub_credentials(hub_id, cbc_db)
+ if not hub_credentials:
+ raise ExtensionHttpError.EXT_012(format_kwargs={'hub_id': hub_id})
+
+ return CBCService(hub_credentials, verify_certificate)
diff --git a/connect_ext_ppr/db.py b/connect_ext_ppr/db.py
index 47ffab9..a9b1fc8 100644
--- a/connect_ext_ppr/db.py
+++ b/connect_ext_ppr/db.py
@@ -220,7 +220,7 @@ def get_cbc_extension_db(engine: Engine = Depends(get_cbc_extension_db_engine)):
)
db: Session = SessionMaker(bind=engine)
try:
- yield db
+ return db
finally:
db.close()
diff --git a/connect_ext_ppr/errors.py b/connect_ext_ppr/errors.py
index a5e8486..fb56e58 100644
--- a/connect_ext_ppr/errors.py
+++ b/connect_ext_ppr/errors.py
@@ -78,19 +78,9 @@ class ExtensionHttpError(ExtensionErrorBase):
5: "Configuration `{obj_id}` cannot be deleted, because related deployment is not synced.",
6: "Can not autogenerate a new PPR for deployment {deployment_id}:"
" There must be one `active` configuration file.",
- 7: "Pricing Batch with ID '{batch_id}' s¡is not found.",
- 8: "More than file found of Pricing Batch '{batch_id}'.",
- 9: "Deployment Hub {hub_id} does not serve Marketplace "
- "{marketplace_id} associated with the batch {batch_id}",
- 10: "Not able to find out Reseller ID for Marketplace {marketplace_id} and Hub {hub_id}.",
- 11: "Deployment {deployment_id} Product ID {d_product_id} and Batch "
- "{batch_id} Product ID {b_product_id} does not match.",
+ # Placeholder 7 - 11
12: "Hub Credentials not found for Hub ID {hub_id}.",
- 13: "Effective Date field not found in Batch {batch_id}.",
- 14: "Effective date {date} is either not found or invalid"
- " for first row in Batch {batch_id}.",
- 15: 'No Marketplace is linked with Deployment Hub {hub_id}',
- 16: "Pricing Batch '{batch_id}' does not have any file.",
+ # Placeholder 13 - 16
17: "Cannot create a new request, an open one already exists.",
18: "Deployment request `{dep_request_id}` can not be retried, newer requests were"
" created for related deployment `{deployment_id}`: {new_requests}.",
@@ -109,3 +99,27 @@ class ExtensionValidationError(ExtensionErrorBase):
" '{target}', allowed {field_name} sources for '{target}' are '{allowed}'.",
6: "Pricing batches invalid: {ids}.",
}
+
+
+class PriceUpdateError(ExtensionErrorBase):
+ PREFIX = 'PLT'
+ ERRORS = {
+ 1: "Pricing Batch with ID '{batch_id}' s¡is not found.",
+ 2: "More than file found of Pricing Batch '{batch_id}'.",
+ 3: "Deployment Hub {hub_id} does not serve Marketplace "
+ "{marketplace_id} associated with the batch {batch_id}.",
+ 4: "Not able to find out Reseller ID for Marketplace {marketplace_id} "
+ "and Hub {hub_id}.",
+ 5: "Pricing Batch output '{batch_id}' does not contain "
+ "either Cost or Price column.",
+ 6: "Effective date '{date}' is either not found or invalid "
+ "for first row in Batch {batch_id}.",
+ 7: "Pricing Batch '{batch_id}' does not have any file.",
+ 8: 'No Marketplace is linked with Deployment Hub {hub_id}.',
+ 9: "Deployment {deployment_id} Product ID {d_product_id} and Batch "
+ "{batch_id} Product ID {b_product_id} does not match.",
+ 10: "Pricing Batch output '{batch_id}' does not contain "
+ "mandatory column: {col_name}.",
+ 11: "Pricing Batch output '{batch_id}' contains invalid value at "
+ "column '{column}' of row '{row}'.",
+ }
diff --git a/connect_ext_ppr/models/enums.py b/connect_ext_ppr/models/enums.py
index 86e3cf5..4bd5d83 100644
--- a/connect_ext_ppr/models/enums.py
+++ b/connect_ext_ppr/models/enums.py
@@ -28,6 +28,8 @@ class TaskTypesChoices(str, enum.Enum):
product_setup = 'product_setup'
apply_and_delegate = 'apply_ppr_and_delegate_to_marketplace'
delegate_to_l2 = 'delegate_to_l2'
+ validate_pricelists = 'validate_pricelists'
+ apply_pricelist = 'apply_pricelist'
class MimeTypeChoices(str, enum.Enum):
diff --git a/connect_ext_ppr/models/task.py b/connect_ext_ppr/models/task.py
index 4b9935f..e1d40cd 100644
--- a/connect_ext_ppr/models/task.py
+++ b/connect_ext_ppr/models/task.py
@@ -5,7 +5,7 @@
from connect_ext_ppr.db import Model
from connect_ext_ppr.models.enums import TasksStatusChoices, TaskTypesChoices
-from connect_ext_ppr.models.deployment import DeploymentRequest
+from connect_ext_ppr.models.deployment import DeploymentRequest, MarketplaceConfiguration
from connect_ext_ppr.models.models_utils import transition
@@ -23,6 +23,7 @@ class Task(Model):
default=STATUSES.pending,
)
deployment_request_id = db.Column(db.ForeignKey(DeploymentRequest.id))
+ marketplace_id = db.Column(db.ForeignKey(MarketplaceConfiguration.id), nullable=True)
title = db.Column(db.String(100))
error_message = db.Column(db.String(4000))
type = db.Column(db.Enum(TaskTypesChoices, validate_strings=True))
@@ -35,6 +36,7 @@ class Task(Model):
aborted_by = db.Column(db.String(20), nullable=True)
deployment_request = relationship(DeploymentRequest, foreign_keys='Task.deployment_request_id')
+ marketplace = relationship(MarketplaceConfiguration, foreign_keys='Task.marketplace_id')
@transition('status', target=STATUSES.aborted, sources=[STATUSES.pending])
def abort(self, by):
diff --git a/connect_ext_ppr/schemas.py b/connect_ext_ppr/schemas.py
index aa72ed5..efe542e 100644
--- a/connect_ext_ppr/schemas.py
+++ b/connect_ext_ppr/schemas.py
@@ -6,11 +6,9 @@
from datetime import datetime
from pydantic import BaseModel, Field, root_validator
-from fastapi import status
from typing import Dict, List, Optional, Union
-from connect_ext_ppr.errors import ExtensionValidationError
from connect_ext_ppr.models.enums import (
ConfigurationStateChoices,
DeploymentRequestStatusChoices,
@@ -69,17 +67,6 @@ class ReferenceSchema(NonNullSchema):
icon: Optional[str]
-class ChoicesSchema(BaseModel):
- choices: Optional[List[PrimaryKeyReference]] = []
- all: bool
-
- @root_validator
- def check_choices_exists_if_all_is_false(cls, values):
- if not values.get('all') and not values.get('choices'):
- raise ExtensionValidationError.VAL_003(status_code=status.HTTP_400_BAD_REQUEST)
- return values
-
-
class ProductSchema(NonNullSchema):
id: str
name: str
@@ -200,10 +187,6 @@ class BatchSchema(NonNullSchema):
stream_updated: Optional[bool]
-class BatchProcessResponseSchema(NonNullSchema):
- task_info: str
-
-
class MarketplaceSchema(NonNullSchema):
id: str
name: str
diff --git a/connect_ext_ppr/service.py b/connect_ext_ppr/service.py
index 3dfaf31..ba9f316 100644
--- a/connect_ext_ppr/service.py
+++ b/connect_ext_ppr/service.py
@@ -336,6 +336,7 @@ def add_new_deployment_request(db, dr_data, deployment, account_id, logger):
db.add(mc)
tasks = []
+
tasks.append(Task(
deployment_request_id=deployment_request.id,
title='Product check up and update',
@@ -348,6 +349,35 @@ def add_new_deployment_request(db, dr_data, deployment, account_id, logger):
type=Task.TYPES.apply_and_delegate,
created_by=account_id,
))
+
+ dep_marketplaces = {m.marketplace: m for m in deployment.marketplaces}
+ req_marketplaces = {m.marketplace: m for m in deployment_request.marketplaces}
+
+ update_prices = False
+ for mp_name, marketplace in req_marketplaces.items():
+ if (
+ (not marketplace.pricelist_id)
+ or (marketplace.pricelist_id == dep_marketplaces[mp_name].pricelist_id)
+ ):
+ continue
+
+ tasks.append(Task(
+ deployment_request=deployment_request,
+ title=f'Apply price list to marketplace {marketplace.marketplace}',
+ marketplace=marketplace,
+ type=Task.TYPES.apply_pricelist,
+ created_by=account_id,
+ ))
+ update_prices = True
+
+ if update_prices:
+ tasks.insert(0, Task(
+ deployment_request_id=deployment_request.id,
+ title='Validate price lists',
+ type=Task.TYPES.validate_pricelists,
+ created_by=account_id,
+ ))
+
if deployment_request.delegate_l2:
tasks.append(Task(
deployment_request_id=deployment_request.id,
diff --git a/connect_ext_ppr/services/cbc_hub.py b/connect_ext_ppr/services/cbc_hub.py
index eb3395d..a316732 100644
--- a/connect_ext_ppr/services/cbc_hub.py
+++ b/connect_ext_ppr/services/cbc_hub.py
@@ -8,7 +8,7 @@
from connect_ext_ppr.client import CBCClient
from connect_ext_ppr.client.auth import APSTokenAuth
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
from connect_ext_ppr.models.cbc_extenstion import HubCredential
@@ -55,7 +55,7 @@ def __validate_client(self):
method='GET',
path=f'{self.hub_credential.controller_url}/aps',
)
- except ClientError:
+ except CBCClientError:
raise ValueError('hub_credential are not valid!')
@cached_property
@@ -192,6 +192,8 @@ def parse_price_file(
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
+ file.seek(0)
+
fcs = self.get_flat_catalog_service(account_id)
return fcs.flat_catalog.price_import_wizard.action(
name=f'upload?vendorId={vendor_id}',
diff --git a/connect_ext_ppr/services/pricing.py b/connect_ext_ppr/services/pricing.py
index ad536b7..1970585 100644
--- a/connect_ext_ppr/services/pricing.py
+++ b/connect_ext_ppr/services/pricing.py
@@ -1,16 +1,101 @@
from datetime import datetime
+from tempfile import NamedTemporaryFile
from typing import Dict
from connect.client import ConnectClient
from connect.client.rql import R
from openpyxl import load_workbook
-from connect_ext_ppr.errors import ExtensionHttpError
-from connect_ext_ppr.services.cbc_extension import get_hub_credentials
-from connect_ext_ppr.services.cbc_hub import CBCService
+from connect_ext_ppr.errors import PriceUpdateError
+from connect_ext_ppr.client.exception import CBCClientError
+from connect_ext_ppr.utils import execute_with_retry
+
+
+PRICELIST_COLUMNS = {
+ 'MPN': str,
+ 'Billing Period': str,
+ 'Cost': (int, float),
+ 'Cost Currency': str,
+ 'Price': (int, float),
+ 'Price Currency': str,
+ 'MSRP': (int, float),
+ 'Effective Date': str,
+}
+
+
+def validate_pricelist_batch(connect_client, batch_id):
+ """
+ @param Client connect_client:
+ @param str batch_id:
+
+ @returns None
+ @raises ClientError
+ """
+ excel_file, file_name = _fetch_batch_output_file(connect_client, batch_id)
+ try:
+ wb = load_workbook(excel_file)
+ ws = wb['Data']
+ _determine_dataset(ws, batch_id)
+ wb.close()
+ finally:
+ excel_file.close()
+
+
+def apply_pricelist_to_marketplace(
+ deployment_request,
+ cbc_service,
+ connect_client,
+ marketplace,
+):
+ """
+ @param DeploymentRequest deployment_request:
+ @param CBCService cbc_service:
+ @param Client connect_client:
+ @param MarketplaceConfiguration marketplace:
+
+ @returns None
+ @raises ClientError, CBCClientError
+ """
+ reseller_id = _identify_reseller_id(
+ client=connect_client,
+ batch_id=marketplace.pricelist_id,
+ marketplace_id=marketplace.marketplace,
+ hub_id=deployment_request.deployment.hub_id,
+ )
+ excel_file, file_name, dataset = _prepare_file(
+ client=connect_client,
+ batch_id=marketplace.pricelist_id,
+ )
+ try:
+ _process_batch(
+ cbc_service=cbc_service,
+ excel_file=excel_file,
+ file_name=file_name,
+ reseller_id=reseller_id,
+ deployment=deployment_request.deployment,
+ dataset=dataset,
+ )
+ finally:
+ excel_file.close()
-def determine_dataset(ws, batch_id):
+def identify_marketplaces(
+ client: ConnectClient,
+ hub_id: str,
+):
+ marketplaces = client.marketplaces.filter(
+ R().hubs.id.eq(hub_id),
+ )
+
+ if not marketplaces:
+ raise PriceUpdateError.PLT_008(
+ format_kwargs={'hub_id': hub_id},
+ )
+
+ return [marketplace['id'] for marketplace in marketplaces]
+
+
+def _determine_dataset(ws, batch_id):
dataset = {
'cost': False,
'price': False,
@@ -18,64 +103,89 @@ def determine_dataset(ws, batch_id):
'effective_date': None,
}
- headers = [cell.value.lower() for cell in ws[1]]
- for key in dataset.keys():
- if key in headers:
- dataset[key] = True
+ headers = {
+ cell.value: i
+ for i, cell in enumerate(ws[1])
+ if cell.value in PRICELIST_COLUMNS
+ }
- if 'effective date' not in headers:
- raise ExtensionHttpError.EXT_013(
- format_kwargs={
+ dataset['msrp'] = 'MSRP' in headers
+
+ _validate_required_columns(headers.keys(), batch_id)
+
+ _validate_value_columns(headers.keys(), batch_id)
+
+ dataset.update(_validate_value_columns(headers.keys(), batch_id))
+
+ _validate_pricelist_content(ws, headers, batch_id)
+
+ dataset['effective_date'] = _parse_effective_date(ws, headers, batch_id)
+
+ return dataset
+
+
+def _validate_required_columns(columns, batch_id):
+ for required_col in ['MPN', 'Billing Period', 'Effective Date']:
+ if required_col not in columns:
+ raise PriceUpdateError.PLT_010(format_kwargs={
'batch_id': batch_id,
- },
- )
+ 'col_name': required_col,
+ })
+
- effective_date_index = headers.index('effective date')
-
- first_row = [str(cell.value).lower() if cell.value else cell.value for cell in ws[2]]
- effective_date_str = first_row[effective_date_index]
- if effective_date_str:
- try:
- effective_date = effective_date_str.split('t')[0]
- dataset['effective_date'] = datetime.strptime(
- effective_date,
- '%Y-%m-%d',
- ).strftime('%m/%d/%Y')
- except (ValueError, KeyError):
- raise ExtensionHttpError.EXT_014(
- format_kwargs={
+def _validate_value_columns(columns, batch_id):
+ dataset = {
+ 'cost': False,
+ 'price': False,
+ }
+
+ for val_col in ['Cost', 'Price']:
+ if val_col in columns:
+ dataset[val_col.lower()] = True
+ if f'{val_col} Currency' not in columns:
+ raise PriceUpdateError.PLT_010(format_kwargs={
'batch_id': batch_id,
- 'date': effective_date_str,
- },
- )
- else:
- raise ExtensionHttpError.EXT_014(
- format_kwargs={
- 'batch_id': batch_id,
- 'date': effective_date_str,
- },
- )
+ 'col_name': f'{val_col} Currency',
+ })
- return dataset
+ if (not dataset['cost']) and (not dataset['price']):
+ raise PriceUpdateError.PLT_005(format_kwargs={
+ 'batch_id': batch_id,
+ })
+ return dataset
-def identify_marketplaces(
- client: ConnectClient,
- hub_id: str,
-):
- marketplaces = client.marketplaces.filter(
- R().hubs.id.eq(hub_id),
- )
- if not marketplaces:
- raise ExtensionHttpError.EXT_015(
- format_kwargs={'hub_id': hub_id},
+def _validate_pricelist_content(ws, columns, batch_id):
+ for row_number, row in enumerate(ws.iter_rows(min_row=2), start=2):
+ for col in columns:
+ if not isinstance(row[columns[col]].value, PRICELIST_COLUMNS[col]):
+ raise PriceUpdateError.PLT_011(format_kwargs={
+ 'batch_id': batch_id,
+ 'column': col,
+ 'row': row_number,
+ })
+
+
+def _parse_effective_date(ws, columns, batch_id):
+ effective_date_str = ws[2][columns['Effective Date']].value
+
+ try:
+ effective_date = effective_date_str.split('T')[0]
+ return datetime.strptime(
+ effective_date,
+ '%Y-%m-%d',
+ ).strftime('%m/%d/%Y')
+ except (ValueError, KeyError):
+ raise PriceUpdateError.PLT_006(
+ format_kwargs={
+ 'batch_id': batch_id,
+ 'date': effective_date_str,
+ },
)
- return [marketplace['id'] for marketplace in marketplaces]
-
-def identify_cbc_hubs(
+def _identify_cbc_hubs(
client: ConnectClient,
marketplace: Dict,
):
@@ -92,7 +202,7 @@ def identify_cbc_hubs(
return []
-def get_reseller_id(marketplace, hub_id):
+def _get_reseller_id(marketplace, hub_id):
hub_details = None
if 'hubs' in marketplace.keys():
hub_details = next(filter(lambda h: h['hub']['id'] == hub_id, marketplace['hubs']))
@@ -101,41 +211,27 @@ def get_reseller_id(marketplace, hub_id):
return hub_details['external_id']
-def prepare_file(client, batch_id):
- files = list(client('pricing').batches[batch_id].files.filter(type='output'))
- if not files:
- raise ExtensionHttpError.EXT_016(
- format_kwargs={'batch_id': batch_id},
- )
+def _prepare_file(client, batch_id):
+ excel_file, file_name = _fetch_batch_output_file(client, batch_id)
+ wb = load_workbook(excel_file)
+ try:
+ ws = wb['Data']
+ dataset = _determine_dataset(ws, batch_id)
+ ws.title = 'Price-list'
+ excel_file.seek(0)
+ wb.save(excel_file)
+ excel_file.seek(0)
+ finally:
+ wb.close()
- if len(files) > 1:
- raise ExtensionHttpError.EXT_008(
- format_kwargs={'batch_id': batch_id},
- )
+ return excel_file, file_name, dataset
- file = files[0]
- file_location = file['name']
- file_content = client.get(
- file_location[11:] if file_location.startswith('/public/v1/') else file_location,
- )
- file_name = f"{file['id']}.xlsx"
-
- with open(file_name, 'wb') as file:
- file.write(file_content)
-
- wb = load_workbook(filename=file_name)
- ws = wb['Data']
- dataset = determine_dataset(ws, batch_id)
- ws.title = 'Price-list'
- wb.save(file_name)
-
- return file_name, dataset
-
-def fetch_and_validate_batch(client, batch_id, deployment):
+# TODO: no used
+def _fetch_and_validate_batch(client, batch_id, deployment):
batches = client('pricing').batches.filter(id=batch_id).select('+stream.context')
if not batches:
- raise ExtensionHttpError.EXT_007(
+ raise PriceUpdateError.PLT_001(
format_kwargs={'batch_id': batch_id},
)
@@ -144,7 +240,7 @@ def fetch_and_validate_batch(client, batch_id, deployment):
product_id = batch['stream']['context']['product']['id']
if product_id != deployment.product_id:
- raise ExtensionHttpError.EXT_011(
+ raise PriceUpdateError.PLT_009(
format_kwargs={
'batch_id': batch_id,
'b_product_id': product_id,
@@ -156,29 +252,54 @@ def fetch_and_validate_batch(client, batch_id, deployment):
return batch
-def identify_reseller_id(client, batch, deployment):
- marketplace_id = batch['stream']['context']['marketplace']['id']
+def _fetch_batch_output_file(client, batch_id):
+ files = list(client('pricing').batches[batch_id].files.filter(type='output'))
+ if not files:
+ raise PriceUpdateError.PLT_007(
+ format_kwargs={'batch_id': batch_id},
+ )
+
+ if len(files) > 1:
+ raise PriceUpdateError.PLT_002(
+ format_kwargs={'batch_id': batch_id},
+ )
+
+ file = files[0]
+ file_location = file['name']
+ file_content = client.get(
+ file_location[11:] if file_location.startswith('/public/v1/') else file_location,
+ )
+ file_name = f"{file['id']}.xlsx"
+
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ excel_file.write(file_content)
+ excel_file.seek(0)
+
+ return excel_file, file_name
+
+
+def _identify_reseller_id(client, batch_id, marketplace_id, hub_id):
marketplace = client.marketplaces[marketplace_id].get()
- marketplace_hubs = identify_cbc_hubs(client, marketplace)
+ marketplace_hubs = _identify_cbc_hubs(client, marketplace)
marketplace_hub_ids = [hub['id'] for hub in marketplace_hubs]
- if deployment.hub_id not in marketplace_hub_ids:
- raise ExtensionHttpError.EXT_009(
+ if hub_id not in marketplace_hub_ids:
+ raise PriceUpdateError.PLT_003(
format_kwargs={
- 'hub_id': deployment.hub_id,
+ 'hub_id': hub_id,
'marketplace_id': marketplace_id,
- 'batch_id': batch['id'],
+ 'batch_id': batch_id,
},
)
- reseller_id = get_reseller_id(marketplace, deployment.hub_id)
+ reseller_id = _get_reseller_id(marketplace, hub_id)
if not reseller_id:
- raise ExtensionHttpError.EXT_010(
+ raise PriceUpdateError.PLT_004(
format_kwargs={
- 'hub_id': deployment.hub_id,
+ 'hub_id': hub_id,
'marketplace_id': marketplace_id,
},
)
@@ -186,42 +307,37 @@ def identify_reseller_id(client, batch, deployment):
return reseller_id
-def process_batch(
- cbc_db,
+def _process_batch(
+ cbc_service,
+ excel_file,
file_name,
reseller_id,
deployment,
dataset,
):
- hub_credentials = get_hub_credentials(deployment.hub_id, cbc_db)
- if not hub_credentials:
- raise ExtensionHttpError.EXT_012(
- format_kwargs={
- 'hub_id': deployment.hub_id,
- },
- )
+ excel_file.seek(0)
- cbc_service = CBCService(hub_credentials, False)
-
- with open(file_name, 'rb') as price_file:
- parsed_price = cbc_service.parse_price_file(
- reseller_id,
- deployment.vendor_id,
- price_file,
- )
+ parsed_price = execute_with_retry(
+ function=cbc_service.parse_price_file,
+ exception_class=CBCClientError,
+ args=(reseller_id, deployment.vendor_id, excel_file),
+ )
- data_id = parsed_price['dataId']
+ data_id = parsed_price['dataId']
- cbc_service.prepare_price_proposal(
- reseller_id,
- parsed_price,
- dataset['cost'],
- dataset['price'],
- dataset['msrp'],
- dataset['effective_date'],
- )
+ execute_with_retry(
+ function=cbc_service.prepare_price_proposal,
+ exception_class=CBCClientError,
+ args=(
+ reseller_id, parsed_price, dataset['cost'],
+ dataset['price'], dataset['msrp'], dataset['effective_date'],
+ ),
+ )
- cbc_service.apply_prices(
+ execute_with_retry(
+ function=cbc_service.apply_prices,
+ exception_class=CBCClientError,
+ args=(
reseller_id,
parsed_price,
dataset['cost'],
@@ -229,6 +345,7 @@ def process_batch(
dataset['msrp'],
dataset['effective_date'],
file_name,
- )
+ ),
+ )
- return data_id
+ return data_id
diff --git a/connect_ext_ppr/static/deployment-details.43c6bbfab7e443946135.js b/connect_ext_ppr/static/deployment-details.b0d7de95e55401ae12ca.js
similarity index 100%
rename from connect_ext_ppr/static/deployment-details.43c6bbfab7e443946135.js
rename to connect_ext_ppr/static/deployment-details.b0d7de95e55401ae12ca.js
diff --git a/connect_ext_ppr/static/index.e6db85b4789728913af1.js b/connect_ext_ppr/static/index.bbc6cee29733e32ecf28.js
similarity index 99%
rename from connect_ext_ppr/static/index.e6db85b4789728913af1.js
rename to connect_ext_ppr/static/index.bbc6cee29733e32ecf28.js
index a5827d0..e918da1 100644
--- a/connect_ext_ppr/static/index.e6db85b4789728913af1.js
+++ b/connect_ext_ppr/static/index.bbc6cee29733e32ecf28.js
@@ -3096,7 +3096,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
/******/ // This function allow to reference async chunks
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
-/******/ return "" + chunkId + "." + {"deployment-details":"43c6bbfab7e443946135","request-details":"3464656fe301d1df40ca"}[chunkId] + ".js";
+/******/ return "" + chunkId + "." + {"deployment-details":"b0d7de95e55401ae12ca","request-details":"2e8575914807cddd3bc9"}[chunkId] + ".js";
/******/ };
/******/ })();
/******/
diff --git a/connect_ext_ppr/static/index.html b/connect_ext_ppr/static/index.html
index ff0e079..85379a1 100644
--- a/connect_ext_ppr/static/index.html
+++ b/connect_ext_ppr/static/index.html
@@ -12,7 +12,7 @@
Index
-
+
diff --git a/connect_ext_ppr/static/request-details.3464656fe301d1df40ca.js b/connect_ext_ppr/static/request-details.2e8575914807cddd3bc9.js
similarity index 100%
rename from connect_ext_ppr/static/request-details.3464656fe301d1df40ca.js
rename to connect_ext_ppr/static/request-details.2e8575914807cddd3bc9.js
diff --git a/connect_ext_ppr/tasks_manager.py b/connect_ext_ppr/tasks_manager.py
index 75245b6..803a22a 100644
--- a/connect_ext_ppr/tasks_manager.py
+++ b/connect_ext_ppr/tasks_manager.py
@@ -2,11 +2,12 @@
from datetime import datetime
from io import BufferedReader
+from connect.client import ClientError
from sqlalchemy.orm import joinedload, selectinload
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
from connect_ext_ppr.constants import PPR_FILE_NAME_DELEGATION_L2
-from connect_ext_ppr.db import get_cbc_extension_db, get_db_ctx_manager
+from connect_ext_ppr.db import get_cbc_extension_db, get_cbc_extension_db_engine, get_db_ctx_manager
from connect_ext_ppr.models.enums import CBCTaskLogStatus
from connect_ext_ppr.models.enums import (
DeploymentRequestStatusChoices,
@@ -14,13 +15,17 @@
TasksStatusChoices,
TaskTypesChoices,
)
-from connect_ext_ppr.models.deployment import DeploymentRequest
+from connect_ext_ppr.models.deployment import DeploymentRequest, MarketplaceConfiguration
from connect_ext_ppr.models.ppr import PPRVersion
from connect_ext_ppr.models.task import Task
-from connect_ext_ppr.services.cbc_extension import get_hub_credentials
-from connect_ext_ppr.services.cbc_hub import CBCService
+from connect_ext_ppr.client.utils import get_cbc_service
+from connect_ext_ppr.services.pricing import (
+ apply_pricelist_to_marketplace,
+ validate_pricelist_batch,
+)
from connect_ext_ppr.utils import (
create_dr_file_to_media,
+ execute_with_retry,
get_base_workbook,
get_file_size,
get_ppr_from_media,
@@ -33,48 +38,52 @@ class TaskException(Exception):
def _get_cbc_service(config, deployment):
- cbc_db = get_cbc_extension_db(config)
- hub_credentials = get_hub_credentials(deployment.hub_id, cbc_db)
- if not hub_credentials:
- raise TaskException(f'Hub Credentials not found for Hub ID {deployment.hub_id}')
-
- return CBCService(hub_credentials, False)
+ cbc_db = get_cbc_extension_db(engine=get_cbc_extension_db_engine(config))
+ try:
+ return get_cbc_service(deployment.hub_id, cbc_db)
+ except ClientError as e:
+ raise TaskException(e.message)
-def _execute_with_retries(function, func_args, num_retries=5):
+def _execute_with_retries(function, func_kwargs, num_retries=5):
"""
@param function: reference to function to execute
- @param func_args: dict with the mapping of function's arguments
+ @param func_kwargs: dict with the mapping of function's arguments
@param num_retries: Max amount of retries
@return function return value
"""
- while num_retries > 0:
- try:
- num_retries -= 1
- return function(**func_args)
- except ClientError as ex:
- if num_retries == 0:
- raise TaskException(str(ex))
+ try:
+ return execute_with_retry(
+ function=function,
+ exception_class=CBCClientError,
+ kwargs=func_kwargs,
+ num_retries=num_retries,
+ )
+ except CBCClientError as ex:
+ raise TaskException(str(ex))
def _send_ppr(cbc_service, file: BufferedReader):
- parsed_ppr = _execute_with_retries(cbc_service.parse_ppr, func_args={'file': file})
+ parsed_ppr = _execute_with_retries(cbc_service.parse_ppr, func_kwargs={'file': file})
if 'error' in parsed_ppr.keys():
raise TaskException(parsed_ppr.get('message'))
- tracking_id = _execute_with_retries(cbc_service.apply_ppr, func_args={'parsed_ppr': parsed_ppr})
+ tracking_id = _execute_with_retries(
+ cbc_service.apply_ppr,
+ func_kwargs={'parsed_ppr': parsed_ppr},
+ )
if not tracking_id:
- raise TaskException('Some error ocurred trying to upload ppr.')
+ raise TaskException('Some error occurred trying to upload ppr.')
return tracking_id
def _check_cbc_task_status(cbc_service, tracking_id):
task_log = _execute_with_retries(
- cbc_service.search_task_logs_by_name, func_args={'partial_name': tracking_id},
+ cbc_service.search_task_logs_by_name, func_kwargs={'partial_name': tracking_id},
)
# Setting this first default value in case takes time to create it in extenal system.
task_log = task_log[0] if task_log else {'status': CBCTaskLogStatus.not_started}
@@ -141,7 +150,7 @@ def check_and_update_product(deployment_request, cbc_service, **kwargs):
product_id = deployment_request.deployment.product_id
response = _execute_with_retries(
- cbc_service.get_product_details, func_args={'product_id': product_id},
+ cbc_service.get_product_details, func_kwargs={'product_id': product_id},
)
if 'error' in response.keys():
@@ -149,7 +158,7 @@ def check_and_update_product(deployment_request, cbc_service, **kwargs):
if response.get('isUpdateAvailable'):
response = _execute_with_retries(
- cbc_service.update_product, func_args={'product_id': product_id},
+ cbc_service.update_product, func_kwargs={'product_id': product_id},
)
if 'error' in response.keys():
@@ -162,6 +171,82 @@ def apply_ppr_and_delegate_to_marketplaces(deployment_request, **kwargs):
return True
+def apply_pricelist_task(
+ deployment_request,
+ cbc_service,
+ connect_client,
+ marketplace,
+ db,
+ **kwargs,
+):
+ """ Applies a price list for a sinle marketplace
+
+ @param DeploymentRequest deployment_request:
+ @param CBCService cbc_service:
+ @param Client connect_client:
+ @param MarketplaceConfiguration marketplace:
+ @param Session db:
+
+ @returns bool
+ @raises TaskException
+ """
+ if not deployment_request.manually:
+ try:
+ apply_pricelist_to_marketplace(
+ deployment_request,
+ cbc_service,
+ connect_client,
+ marketplace,
+ )
+ except (ClientError, CBCClientError) as e:
+ raise TaskException(f'Error while processing pricelist: {e}')
+
+ deployment_marketplace = db.query(MarketplaceConfiguration).filter_by(
+ deployment_id=deployment_request.deployment_id,
+ ).with_for_update().one()
+ deployment_marketplace.pricelist_id = marketplace.pricelist_id
+ db.add(deployment_marketplace)
+ db.commit()
+
+ return True
+
+
+def validate_pricelists_task(
+ deployment_request,
+ connect_client,
+ **kwargs,
+):
+ """ Validates all price lists of deployment request
+
+ @param DeploymentRequest deployment_request:
+ @param Client connect_client:
+
+ @returns bool
+ @raises TaskException
+ """
+ dep_marketplaces = {mp.marketplace: mp for mp in deployment_request.deployment.marketplaces}
+
+ for marketplace in deployment_request.marketplaces:
+ if (
+ (not marketplace.pricelist_id)
+ or marketplace.pricelist_id == dep_marketplaces[marketplace.marketplace].pricelist_id
+ ):
+ continue
+
+ try:
+ validate_pricelist_batch(connect_client, marketplace.pricelist_id)
+ except ClientError as e:
+ raise TaskException(
+ 'Price list {pl_id} of marketplace {mp} validation failed: {msg}'.format(
+ pl_id=marketplace.pricelist_id,
+ mp=marketplace.marketplace,
+ msg=e.message,
+ ),
+ )
+
+ return True
+
+
def delegate_to_l2(deployment_request, cbc_service, connect_client, **kwargs):
if deployment_request.manually:
return True
@@ -193,12 +278,14 @@ def delegate_to_l2(deployment_request, cbc_service, connect_client, **kwargs):
TaskTypesChoices.product_setup: check_and_update_product,
TaskTypesChoices.apply_and_delegate: apply_ppr_and_delegate_to_marketplaces,
TaskTypesChoices.delegate_to_l2: delegate_to_l2,
+ TaskTypesChoices.validate_pricelists: validate_pricelists_task,
+ TaskTypesChoices.apply_pricelist: apply_pricelist_task,
}
-def execute_tasks(db, config, tasks, connect_client):
+def execute_tasks(db, config, tasks, connect_client): # noqa: CCR001
was_succesfull = False
- cbc_service = _get_cbc_service(config, tasks[0].deployment_request.deployment)
+ cbc_service = None
for task in tasks:
db.refresh(task, with_for_update=True)
@@ -209,10 +296,17 @@ def execute_tasks(db, config, tasks, connect_client):
db.commit()
try:
+ if not cbc_service:
+ cbc_service = _get_cbc_service(
+ config=config,
+ deployment=task.deployment_request.deployment,
+ )
was_succesfull = TASK_PER_TYPE.get(task.type)(
deployment_request=task.deployment_request,
cbc_service=cbc_service,
connect_client=connect_client,
+ marketplace=task.marketplace,
+ db=db,
)
task.status = TasksStatusChoices.done
if not was_succesfull:
@@ -221,9 +315,9 @@ def execute_tasks(db, config, tasks, connect_client):
was_succesfull = False
task.error_message = str(ex)
task.status = TasksStatusChoices.error
- except Exception:
+ except Exception as err:
was_succesfull = False
- task.error_message = 'Unexpected error'
+ task.error_message = str(err)
task.status = TasksStatusChoices.error
task.finished_at = datetime.utcnow()
diff --git a/connect_ext_ppr/utils.py b/connect_ext_ppr/utils.py
index de6bd04..05a6fd7 100644
--- a/connect_ext_ppr/utils.py
+++ b/connect_ext_ppr/utils.py
@@ -660,3 +660,25 @@ def process_ppr_file_for_delelegate_l2(sheet_name, ws):
ws['Published'] = 'TRUE'
elif sheet_name == 'ServicePlans':
ws['Published'] = 'FALSE'
+
+
+def execute_with_retry(function, exception_class, args=None, kwargs=None, num_retries=5):
+ """
+ @param function: reference to function to execute
+ @param exception_class: retry the function call if this exception occurred
+ @param args: list with function's arguments
+ @param kwargs: dict function's named arguments
+ @param num_retries: Max amount of retries
+
+ @return function return value
+ """
+ while num_retries > 0:
+ try:
+ num_retries -= 1
+ return function(
+ *(args or []),
+ **(kwargs or {}),
+ )
+ except exception_class:
+ if num_retries == 0:
+ raise
diff --git a/connect_ext_ppr/validator.py b/connect_ext_ppr/validator.py
index bd004e9..fa1d79a 100644
--- a/connect_ext_ppr/validator.py
+++ b/connect_ext_ppr/validator.py
@@ -38,10 +38,10 @@ def validate_dr_marketplaces(client, product_id, dr_marketplaces, dep_marketplac
format_kwargs={'field': 'marketplace'},
)
- mp_configs = {mp.id: mp for mp in dr_marketplaces}
- dep_ids = {mp.marketplace for mp in dep_marketplaces}
+ req_mp_configs = {mp.id: mp for mp in dr_marketplaces}
+ dep_mp_configs = {mp.marketplace: mp for mp in dep_marketplaces}
- diff = list(mp_configs.keys() - dep_ids)
+ diff = list(req_mp_configs.keys() - dep_mp_configs.keys())
if diff:
diff.sort()
raise ExtensionValidationError.VAL_002(
@@ -52,14 +52,14 @@ def validate_dr_marketplaces(client, product_id, dr_marketplaces, dep_marketplac
status_code=status.HTTP_400_BAD_REQUEST,
)
- validate_pricelist_ids(client, product_id, mp_configs)
+ validate_pricelist_ids(client, product_id, req_mp_configs, dep_mp_configs)
-def validate_pricelist_ids(client, product_id, mp_configs):
+def validate_pricelist_ids(client, product_id, req_mp_configs, dep_mp_configs):
rql_filters = []
pricelist_ids = set()
- for mp_id, config in mp_configs.items():
- if config.pricelist:
+ for mp_id, config in req_mp_configs.items():
+ if config.pricelist and config.pricelist.id != dep_mp_configs[mp_id].pricelist_id:
pricelist_ids.add(config.pricelist.id)
rql_filters.append(
R().stream.context.marketplace.id.eq(mp_id)
diff --git a/connect_ext_ppr/webapp.py b/connect_ext_ppr/webapp.py
index 51a13af..7a4cfb3 100644
--- a/connect_ext_ppr/webapp.py
+++ b/connect_ext_ppr/webapp.py
@@ -4,7 +4,7 @@
# All rights reserved.
#
from concurrent.futures import ThreadPoolExecutor
-from logging import Logger, LoggerAdapter
+from logging import Logger
from typing import List
from connect.client import ConnectClient
@@ -22,16 +22,13 @@
)
from connect.eaas.core.extension import WebApplicationBase
from fastapi import Depends, Request, Response, status
-from fastapi.responses import JSONResponse
from fastapi_filter import FilterDepends
from sqlalchemy import desc, exists
from sqlalchemy.exc import SQLAlchemyError
-from sqlalchemy.orm import joinedload, selectinload, Session
+from sqlalchemy.orm import joinedload, selectinload
-from connect_ext_ppr.client.exception import ClientError
from connect_ext_ppr.db import (
create_db,
- get_cbc_extension_db,
get_config,
get_db,
VerboseBaseSession,
@@ -62,7 +59,6 @@
validate_configuration,
)
from connect_ext_ppr.schemas import (
- BatchProcessResponseSchema,
BatchSchema,
ConfigurationCreateSchema,
ConfigurationSchema,
@@ -76,13 +72,7 @@
ProductSchema,
TaskSchema,
)
-from connect_ext_ppr.services.pricing import (
- fetch_and_validate_batch,
- identify_marketplaces,
- identify_reseller_id,
- prepare_file,
- process_batch,
-)
+from connect_ext_ppr.services.pricing import identify_marketplaces
from connect_ext_ppr.tasks_manager import main_process
from connect_ext_ppr.utils import (
_get_extension_client,
@@ -798,46 +788,6 @@ def get_deployment_batches(
return [BatchSchema(**b) for b in batches]
- @router.post(
- '/deployments/{deployment_id}/pricing/batches/{batch_id}/process',
- summary='Process Pricing Batch for Deployment',
- response_model=BatchProcessResponseSchema,
- )
- def process_pricing_batch(
- self,
- deployment_id: str,
- batch_id: str,
- client: ConnectClient = Depends(get_installation_client),
- db: VerboseBaseSession = Depends(get_db),
- cbc_db: Session = Depends(get_cbc_extension_db),
- installation: dict = Depends(get_installation),
- logger: LoggerAdapter = Depends(get_logger),
- ):
- try:
- deployment = get_deployment_by_id(deployment_id, db, installation)
- batch = fetch_and_validate_batch(client, batch_id, deployment)
- reseller_id = identify_reseller_id(client, batch, deployment)
- file_name, dataset = prepare_file(client, batch_id)
-
- data_id = process_batch(
- cbc_db,
- file_name,
- reseller_id,
- deployment,
- dataset,
- )
-
- response = BatchProcessResponseSchema(
- task_info=f'/flat-catalog/price-import-wizard/{data_id}/set-prices',
- )
- return JSONResponse(
- status_code=202,
- content=response.dict(),
- )
- except ClientError as e:
- logger.exception(f'Error while uploading price file for {batch_id}')
- return JSONResponse(status_code=400, content=e.json if e.json else {})
-
@classmethod
def on_startup(cls, logger, config):
# When database schema is completely defined
diff --git a/tests/api/test_deployment_requests.py b/tests/api/test_deployment_requests.py
index 24a5070..b47f14e 100644
--- a/tests/api/test_deployment_requests.py
+++ b/tests/api/test_deployment_requests.py
@@ -447,6 +447,9 @@ def test_create_deployment_request(
marketplace_config_factory(deployment=dep, marketplace_id='MP-124')
marketplace_config_factory(deployment=dep, marketplace_id='MP-123', ppr_id=ppr.id)
marketplace_config_factory(deployment=dep, marketplace_id='MP-234', ppr_id=ppr.id)
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-345', ppr_id=ppr.id, pricelist_id='BAT-222',
+ )
mocker.patch('connect_ext_ppr.webapp.get_client_object', side_effect=[hub_data])
client_mocker = client_mocker_factory(base_url=connect_client.endpoint)
@@ -471,8 +474,9 @@ def test_create_deployment_request(
'manually': True,
'delegate_l2': True,
'marketplaces': [
- {'id': 'MP-123'},
- {'id': 'MP-234', 'pricelist': {'id': 'BAT-111'}},
+ {'id': 'MP-123'}, # pricelist not defined. skip
+ {'id': 'MP-234', 'pricelist': {'id': 'BAT-111'}}, # appy pricelist
+ {'id': 'MP-345', 'pricelist': {'id': 'BAT-222'}}, # already applied pricelist. skip
],
}
@@ -532,13 +536,18 @@ def test_create_deployment_request(
).count() == 1
tasks = dbsession.query(Task).order_by(Task.id)
- assert tasks.count() == 3
+ assert tasks.count() == 5
assert tasks[0].id == f'TSK-{deployment_request.id[5:]}-000'
- assert tasks[0].type == Task.TYPES.product_setup
+ assert tasks[0].type == Task.TYPES.validate_pricelists
assert tasks[1].id == f'TSK-{deployment_request.id[5:]}-001'
- assert tasks[1].type == Task.TYPES.apply_and_delegate
+ assert tasks[1].type == Task.TYPES.product_setup
assert tasks[2].id == f'TSK-{deployment_request.id[5:]}-002'
- assert tasks[2].type == Task.TYPES.delegate_to_l2
+ assert tasks[2].type == Task.TYPES.apply_and_delegate
+ assert tasks[3].id == f'TSK-{deployment_request.id[5:]}-003'
+ assert tasks[3].type == Task.TYPES.apply_pricelist
+ assert tasks[3].title == 'Apply price list to marketplace MP-234'
+ assert tasks[4].id == f'TSK-{deployment_request.id[5:]}-004'
+ assert tasks[4].type == Task.TYPES.delegate_to_l2
def test_create_deployment_request_without_delegation_to_l2(
diff --git a/tests/api/test_pricing_batches.py b/tests/api/test_pricing_batches.py
deleted file mode 100644
index 107791c..0000000
--- a/tests/api/test_pricing_batches.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from unittest import TestCase
-from unittest.mock import patch
-
-from connect_ext_ppr.client.exception import ClientError
-
-
-@patch('connect_ext_ppr.webapp.get_deployment_by_id')
-@patch('connect_ext_ppr.webapp.fetch_and_validate_batch')
-@patch('connect_ext_ppr.webapp.identify_reseller_id', return_value='1000001')
-@patch('connect_ext_ppr.webapp.prepare_file', return_value=('File.xlsx', {}))
-@patch('connect_ext_ppr.webapp.process_batch', return_value=1)
-def test_process_pricing_batch_positive(
- mock_process_batch,
- mock_prepare_file,
- mock_identify_reseller_id,
- mock_fetch_and_validate_batch,
- mock_get_deployment_by_id,
- api_client,
- deployment,
- installation,
- batch,
-):
- mock_get_deployment_by_id.return_value = deployment
- mock_fetch_and_validate_batch.return_value = batch
-
- response = api_client.post(
- f'/api/deployments/{deployment.id}/pricing/batches/BT-000-000-000/process',
- installation=installation,
- )
-
- assert response.status_code == 202
- TestCase().assertDictEqual(
- response.json(),
- {'task_info': '/flat-catalog/price-import-wizard/1/set-prices'},
- )
-
-
-@patch('connect_ext_ppr.webapp.get_deployment_by_id')
-@patch('connect_ext_ppr.webapp.fetch_and_validate_batch')
-def test_process_pricing_batch_negative(
- mock_fetch_and_validate_batch,
- mock_get_deployment_by_id,
- api_client,
- deployment,
- installation,
- batch,
-):
- mock_get_deployment_by_id.return_value = deployment
- mock_fetch_and_validate_batch.side_effect = ClientError(
- 'Unexpected error during batch validation.')
-
- response = api_client.post(
- f'/api/deployments/{deployment.id}/pricing/batches/BT-000-000-000/process',
- installation=installation,
- )
-
- assert response.status_code == 400
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index d6b7c2f..d6a38b4 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -7,7 +7,7 @@
from requests_oauthlib import OAuth1
from connect_ext_ppr.client import CBCClient
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
@responses.activate
@@ -128,5 +128,5 @@ def test_collection_service_discovery_invalid_type(cbc_client):
@patch.object(Session, 'send', side_effect=Exception('Mock error!'))
def test_client_get_connection_error(mock_send, cbc_client):
- with pytest.raises(ClientError):
+ with pytest.raises(CBCClientError):
cbc_client.get('identifier')
diff --git a/tests/client/test_service.py b/tests/client/test_service.py
index 6a892ab..743e94f 100644
--- a/tests/client/test_service.py
+++ b/tests/client/test_service.py
@@ -3,7 +3,7 @@
import pytest
import responses
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
@responses.activate
@@ -35,7 +35,7 @@ def test_service_discovery_client_error(
status=401,
)
- with pytest.raises(ClientError):
+ with pytest.raises(CBCClientError):
cbc_client(flat_catalog_type).get()
diff --git a/tests/client/test_utils.py b/tests/client/test_utils.py
new file mode 100644
index 0000000..7fee8b3
--- /dev/null
+++ b/tests/client/test_utils.py
@@ -0,0 +1,33 @@
+from unittest.mock import patch
+
+import pytest
+
+from connect_ext_ppr.errors import ClientError
+from connect_ext_ppr.client.utils import CBCService
+from connect_ext_ppr.client.utils import get_cbc_service
+
+
+@patch.object(CBCService, '__init__', return_value=None)
+@patch('connect_ext_ppr.client.utils.get_hub_credentials')
+def test_get_cbc_service_ok(
+ mock_hub_cred,
+ mock_service,
+ cbc_db_session,
+):
+ service = get_cbc_service('HUB-0', cbc_db_session, True)
+
+ assert isinstance(service, CBCService)
+ assert mock_hub_cred.called_once_with('HUB-0', cbc_db_session)
+
+
+@patch.object(CBCService, '__init__', return_value=None)
+@patch('connect_ext_ppr.client.utils.get_hub_credentials', return_value=None)
+def test_get_cbc_service_failed(
+ mock_hub_cred,
+ mock_service,
+ cbc_db_session,
+):
+ with pytest.raises(ClientError) as e:
+ get_cbc_service('HUB-0', cbc_db_session, True)
+
+ assert e.value.message == 'Hub Credentials not found for Hub ID HUB-0.'
diff --git a/tests/services/test_cbc_hub.py b/tests/services/test_cbc_hub.py
index d8cbf35..2b63027 100644
--- a/tests/services/test_cbc_hub.py
+++ b/tests/services/test_cbc_hub.py
@@ -5,7 +5,7 @@
import pytest
import responses
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
from connect_ext_ppr.client.ns import Service
from connect_ext_ppr.services.cbc_hub import CBCService
@@ -88,7 +88,7 @@ def test_get_product_details_not_found(
)
cbc_service = CBCService(hub_credentials)
- with pytest.raises(ClientError):
+ with pytest.raises(CBCClientError):
cbc_service.get_product_details(product_id)
@@ -159,7 +159,7 @@ def test_install_product_not_found(
)
cbc_service = CBCService(hub_credentials)
- with pytest.raises(ClientError):
+ with pytest.raises(CBCClientError):
cbc_service.install_product(product_id)
@@ -232,7 +232,7 @@ def test_update_product_negative_product_not_installed(
)
cbc_service = CBCService(hub_credentials)
- with pytest.raises(ClientError):
+ with pytest.raises(CBCClientError):
cbc_service.update_product(product_id)
diff --git a/tests/services/test_pricing.py b/tests/services/test_pricing.py
index cabdfa0..ccaac9f 100644
--- a/tests/services/test_pricing.py
+++ b/tests/services/test_pricing.py
@@ -1,7 +1,9 @@
from copy import deepcopy
+from tempfile import NamedTemporaryFile
from unittest import TestCase
from unittest.mock import patch
+import openpyxl
import pytest
import responses
from connect.client import ClientError
@@ -10,16 +12,250 @@
from connect_ext_ppr.services.cbc_hub import CBCService
from connect_ext_ppr.services.pricing import (
- determine_dataset,
- fetch_and_validate_batch,
- get_reseller_id,
- identify_marketplaces,
- identify_reseller_id,
- prepare_file,
- process_batch,
+ _determine_dataset,
+ _fetch_and_validate_batch,
+ _get_reseller_id,
+ _identify_reseller_id,
+ _prepare_file,
+ _process_batch,
+ apply_pricelist_to_marketplace,
+ identify_marketplaces, validate_pricelist_batch,
)
+@pytest.fixture
+@patch.object(CBCService, 'parse_price_file')
+@patch.object(CBCService, 'prepare_price_proposal')
+@patch.object(CBCService, 'apply_prices')
+@patch.object(CBCService, '__init__')
+def cbc_service(
+ mock___init__,
+ mock_apply_prices,
+ mock_prepare_price_proposal,
+ mock_parse_price_file,
+ parse_price_file_response,
+ price_proposal_response,
+):
+ mock_parse_price_file.return_value = parse_price_file_response
+ mock_prepare_price_proposal.return_value = price_proposal_response
+ mock_apply_prices.return_value = 'Request Accepted.'
+ mock___init__.return_value = None
+
+ return CBCService()
+
+
+@patch('connect_ext_ppr.services.pricing._fetch_batch_output_file')
+def test_validate_pricelist_positive(mock_fetch_file):
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ wb = openpyxl.load_workbook('./tests/fixtures/MFL-0000-0000-0000.xlsx')
+ wb.save(excel_file)
+ excel_file.seek(0)
+
+ mock_fetch_file.return_value = (excel_file, 'filename.xlsx')
+
+ validate_pricelist_batch(excel_file, 'BAT-1')
+
+ assert excel_file.closed
+
+
+@pytest.mark.parametrize('col_id,col_name', (
+ (1, 'Billing Period'),
+ (4, 'MPN'),
+ (16, 'Effective Date'),
+))
+@patch('connect_ext_ppr.services.pricing._fetch_batch_output_file')
+def test_validate_pricelist_negative_missed_req_cols(
+ mock_fetch_file,
+ col_id,
+ col_name,
+):
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ wb = openpyxl.load_workbook('./tests/fixtures/MFL-0000-0000-0000.xlsx')
+ wb['Data'].delete_cols(col_id)
+ wb.save(excel_file)
+ excel_file.seek(0)
+
+ mock_fetch_file.return_value = (excel_file, 'filename.xlsx')
+
+ with pytest.raises(ClientError) as e:
+ validate_pricelist_batch(excel_file, 'BAT-1')
+
+ assert excel_file.closed
+
+ assert e.value.message == (
+ "Pricing Batch output 'BAT-1' does not "
+ f"contain mandatory column: {col_name}."
+ )
+
+
+@patch('connect_ext_ppr.services.pricing._fetch_batch_output_file')
+def test_validate_pricelist_negative_no_cost_price(mock_fetch_file):
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ wb = openpyxl.load_workbook('./tests/fixtures/MFL-0000-0000-0000.xlsx')
+ wb['Data'].delete_cols(7) # Cost
+ wb['Data'].delete_cols(16) # Price
+ wb.save(excel_file)
+ excel_file.seek(0)
+
+ mock_fetch_file.return_value = (excel_file, 'filename.xlsx')
+
+ with pytest.raises(ClientError) as e:
+ validate_pricelist_batch(excel_file, 'BAT-1')
+
+ assert e.value.message == (
+ "Pricing Batch output 'BAT-1' does not "
+ "contain either Cost or Price column."
+ )
+
+
+@pytest.mark.parametrize('col_id,col_name', (
+ (12, 'Cost Currency'),
+ (13, 'Price Currency'),
+))
+@patch('connect_ext_ppr.services.pricing._fetch_batch_output_file')
+def test_validate_pricelist_negative_no_cost_price_currency(
+ mock_fetch_file,
+ col_id,
+ col_name,
+):
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ wb = openpyxl.load_workbook('./tests/fixtures/MFL-0000-0000-0000.xlsx')
+ wb['Data'].delete_cols(col_id) # Cost
+ wb.save(excel_file)
+ excel_file.seek(0)
+
+ mock_fetch_file.return_value = (excel_file, 'filename.xlsx')
+
+ with pytest.raises(ClientError) as e:
+ validate_pricelist_batch(excel_file, 'BAT-1')
+
+ assert e.value.message == (
+ "Pricing Batch output 'BAT-1' does not "
+ f"contain mandatory column: {col_name}."
+ )
+
+
+@pytest.mark.parametrize('col_id,col_name,value', (
+ (7, 'Cost', None),
+ (17, 'Price', 'not number'),
+ (4, 'MPN', None),
+))
+@patch('connect_ext_ppr.services.pricing._fetch_batch_output_file')
+def test_validate_pricelist_negative_invalid_value(
+ mock_fetch_file,
+ col_id,
+ col_name,
+ value,
+):
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ wb = openpyxl.load_workbook('./tests/fixtures/MFL-0000-0000-0000.xlsx')
+ wb['Data'].cell(row=2, column=col_id).value = value
+ wb.save(excel_file)
+ excel_file.seek(0)
+
+ mock_fetch_file.return_value = (excel_file, 'filename.xlsx')
+
+ with pytest.raises(ClientError) as e:
+ validate_pricelist_batch(excel_file, 'BAT-1')
+
+ assert e.value.message == (
+ "Pricing Batch output 'BAT-1' contains invalid "
+ f"value at column '{col_name}' of row '2'."
+ )
+
+
+@patch('connect_ext_ppr.services.pricing._fetch_batch_output_file')
+def test_validate_pricelist_negative_invalid_effective_date(mock_fetch_file):
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ wb = openpyxl.load_workbook('./tests/fixtures/MFL-0000-0000-0000.xlsx')
+ wb['Data'].cell(row=2, column=16).value = 'Not a date'
+ wb.save(excel_file)
+ excel_file.seek(0)
+
+ mock_fetch_file.return_value = (excel_file, 'filename.xlsx')
+
+ with pytest.raises(ClientError) as e:
+ validate_pricelist_batch(excel_file, 'BAT-1')
+
+ assert e.value.message == (
+ "Effective date 'Not a date' is either not "
+ "found or invalid for first row in Batch BAT-1."
+ )
+
+
+def test_apply_pricelist_to_marketplace_positive(
+ mocker,
+ marketplace,
+ cbc_service,
+ batch_output_file,
+ connect_client,
+ client_mocker_factory,
+ deployment_request_factory,
+ marketplace_config_factory,
+):
+ dep_req = deployment_request_factory()
+ mp_conf = marketplace_config_factory('MP-123', deployment_request=dep_req)
+
+ price_excel_file = NamedTemporaryFile(suffix='.xlsx')
+ price_excel_file.read()
+ price_excel_file.seek(0)
+
+ reseller_id_mock = mocker.patch(
+ 'connect_ext_ppr.services.pricing._identify_reseller_id',
+ return_value=marketplace['hubs'][0]['external_id'],
+ )
+ prepare_file_mock = mocker.patch(
+ 'connect_ext_ppr.services.pricing._prepare_file',
+ return_value=(
+ price_excel_file,
+ 'MFL-001.xlsx',
+ {
+ 'cost': True,
+ 'price': True,
+ 'msrp': True,
+ 'effective_date': '07/26/2023',
+ },
+ ),
+ )
+ process_batch_mock = mocker.patch(
+ 'connect_ext_ppr.services.pricing._process_batch',
+ return_value=None,
+ )
+
+ apply_pricelist_to_marketplace(
+ dep_req,
+ cbc_service,
+ connect_client,
+ mp_conf,
+ )
+
+ reseller_id_mock.assert_called_once_with(
+ client=connect_client,
+ batch_id=mp_conf.pricelist_id,
+ marketplace_id=mp_conf.marketplace,
+ hub_id=dep_req.deployment.hub_id,
+ )
+ prepare_file_mock.assert_called_once_with(
+ client=connect_client,
+ batch_id=mp_conf.pricelist_id,
+ )
+ process_batch_mock.assert_called_once_with(
+ cbc_service=cbc_service,
+ excel_file=price_excel_file,
+ file_name='MFL-001.xlsx',
+ reseller_id=marketplace['hubs'][0]['external_id'],
+ deployment=dep_req.deployment,
+ dataset={
+ 'cost': True,
+ 'price': True,
+ 'msrp': True,
+ 'effective_date': '07/26/2023',
+ },
+ )
+
+ assert price_excel_file
+
+
def test_identify_marketplaces_positive(
connect_client,
client_mocker_factory,
@@ -78,8 +314,9 @@ def test_prepare_file_positive(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
)
- file_name, dataset = prepare_file(connect_client, batch_id)
+ excel_file, file_name, dataset = _prepare_file(connect_client, batch_id)
+ assert len(excel_file.read())
assert file_name == f"{batch_file['id']}.xlsx"
TestCase().assertDictEqual(
dataset,
@@ -106,7 +343,7 @@ def test_prepare_file_negative_no_batch_file(
)
with pytest.raises(ClientError):
- prepare_file(connect_client, batch_id)
+ _prepare_file(connect_client, batch_id)
def test_prepare_file_negative_more_than_one_batch_file(
@@ -124,7 +361,7 @@ def test_prepare_file_negative_more_than_one_batch_file(
)
with pytest.raises(ClientError):
- prepare_file(connect_client, batch_id)
+ _prepare_file(connect_client, batch_id)
def test_fetch_and_validate_batch_positive(
@@ -144,7 +381,7 @@ def test_fetch_and_validate_batch_positive(
return_value=[batch],
)
- response = fetch_and_validate_batch(
+ response = _fetch_and_validate_batch(
connect_client,
batch_id,
no_db_deployment,
@@ -169,7 +406,7 @@ def test_fetch_and_validate_batch_negative_no_batch(
)
with pytest.raises(ClientError):
- fetch_and_validate_batch(
+ _fetch_and_validate_batch(
connect_client,
batch_id,
None,
@@ -196,7 +433,7 @@ def test_fetch_and_validate_batch_negative_batch_does_not_belong_to_deployment(
)
with pytest.raises(ClientError):
- fetch_and_validate_batch(
+ _fetch_and_validate_batch(
connect_client,
batch_id,
deployment,
@@ -224,7 +461,12 @@ def test_identify_reseller_id_positive(
return_value=[hub],
)
- reseller_id = identify_reseller_id(connect_client, batch, no_db_deployment)
+ reseller_id = _identify_reseller_id(
+ connect_client,
+ batch['id'],
+ marketplace_id,
+ no_db_deployment.hub_id,
+ )
assert reseller_id == marketplace['hubs'][0]['external_id']
@@ -250,7 +492,12 @@ def test_identify_reseller_id_negative_deployment_hub_not_present_in_batch_marke
)
with pytest.raises(ClientError):
- identify_reseller_id(connect_client, batch, no_db_deployment)
+ _identify_reseller_id(
+ connect_client,
+ batch['id'],
+ marketplace_id,
+ no_db_deployment.hub_id,
+ )
def test_identify_reseller_id_negative_hubs_not_configured_in_batch_marketplace(
@@ -270,7 +517,12 @@ def test_identify_reseller_id_negative_hubs_not_configured_in_batch_marketplace(
)
with pytest.raises(ClientError):
- identify_reseller_id(connect_client, batch, no_db_deployment)
+ _identify_reseller_id(
+ connect_client,
+ batch['id'],
+ marketplace_id,
+ no_db_deployment.hub_id,
+ )
def test_get_reseller_id_negative_hubs_not_configured(
@@ -279,7 +531,7 @@ def test_get_reseller_id_negative_hubs_not_configured(
modified_marketplace = deepcopy(marketplace)
modified_marketplace.pop('hubs')
- reseller_id = get_reseller_id(modified_marketplace, 'HUB-0000-0000')
+ reseller_id = _get_reseller_id(modified_marketplace, 'HUB-0000-0000')
assert reseller_id is None
@@ -308,7 +560,12 @@ def test_identify_reseller_id_negative_reseller_id_not_mapped(
)
with pytest.raises(ClientError):
- identify_reseller_id(connect_client, batch, no_db_deployment)
+ _identify_reseller_id(
+ connect_client,
+ batch['id'],
+ marketplace_id,
+ no_db_deployment.hub_id,
+ )
@patch.object(CBCService, 'parse_price_file')
@@ -322,7 +579,7 @@ def test_process_batch_positive(
mock_parse_price_file,
parse_price_file_response,
price_proposal_response,
- cbc_db_session,
+ hub_credentials,
no_db_deployment,
batch_dataset,
):
@@ -333,9 +590,14 @@ def test_process_batch_positive(
mock_apply_prices.return_value = 'Request Accepted.'
mock___init__.return_value = None
- data_id = process_batch(
- cbc_db_session,
- './tests/fixtures/MFL-0000-0000-0000.xlsx',
+ excel_file = NamedTemporaryFile(suffix='.xlsx')
+ excel_file.write(open('./tests/fixtures/MFL-0000-0000-0000.xlsx', 'rb').read())
+ excel_file.seek(0)
+
+ data_id = _process_batch(
+ CBCService(hub_credentials),
+ excel_file,
+ 'MFL-0000-0000-0000.xlsx',
reseller_id,
no_db_deployment,
batch_dataset,
@@ -343,22 +605,57 @@ def test_process_batch_positive(
assert data_id == parse_price_file_response['dataId']
-
-def test_process_batch_negative_hub_credential_not_found(
- cbc_db_session,
- no_db_deployment,
-):
- deployment = deepcopy(no_db_deployment)
- deployment.hub_id = 'HB-0000-0001'
-
- with pytest.raises(ClientError):
- process_batch(
- cbc_db_session,
- None,
- None,
- deployment,
- None,
- )
+ assert mock_parse_price_file.call_args[0] == (
+ '10000001',
+ 'VA-000-000',
+ excel_file,
+ )
+ assert mock_prepare_price_proposal.call_args[0] == (
+ '10000001',
+ {
+ 'status': 'PARSED',
+ 'priceListStructure': [
+ 'MPN', 'Vendor ID', 'Vendor Name', 'Service Template / Product Line',
+ 'Product', 'Billing Period', 'Subscription Period', 'Billing Model',
+ 'UOM', 'Lower Limit', 'Effective Date', 'Cost Billing Period',
+ 'Cost Currency', 'Cost', 'Price Currency', 'Price', 'MSRP',
+ 'Margin', 'Reseller Margin', 'Fee Type', 'Subscriptions',
+ 'Seats', 'Active',
+ ],
+ 'pricingModel': 'FLAT',
+ 'feeType': 'RECURRING',
+ 'vendorId': 'VA-000-000',
+ 'dataId': 1,
+ },
+ True,
+ True,
+ True,
+ '07/26/2023',
+ )
+ assert mock_apply_prices.call_args[0] == (
+ '10000001',
+ {
+ 'status': 'PARSED',
+ 'priceListStructure': [
+ 'MPN', 'Vendor ID', 'Vendor Name', 'Service Template / Product Line',
+ 'Product', 'Billing Period', 'Subscription Period',
+ 'Billing Model', 'UOM', 'Lower Limit', 'Effective Date',
+ 'Cost Billing Period', 'Cost Currency', 'Cost',
+ 'Price Currency', 'Price', 'MSRP', 'Margin',
+ 'Reseller Margin', 'Fee Type', 'Subscriptions',
+ 'Seats', 'Active',
+ ],
+ 'pricingModel': 'FLAT',
+ 'feeType': 'RECURRING',
+ 'vendorId': 'VA-000-000',
+ 'dataId': 1,
+ },
+ True,
+ True,
+ True,
+ '07/26/2023',
+ 'MFL-0000-0000-0000.xlsx',
+ )
def test_determine_dataset_negative_effective_date_column_not_present():
@@ -367,7 +664,7 @@ def test_determine_dataset_negative_effective_date_column_not_present():
)
ws = wb['Data']
with pytest.raises(ClientError):
- determine_dataset(ws, 'BAT-000-000-000')
+ _determine_dataset(ws, 'BAT-000-000-000')
def test_determine_dataset_negative_empty_effective_date():
@@ -376,7 +673,7 @@ def test_determine_dataset_negative_empty_effective_date():
)
ws = wb['Data']
with pytest.raises(ClientError):
- determine_dataset(ws, 'BAT-000-000-000')
+ _determine_dataset(ws, 'BAT-000-000-000')
def test_determine_dataset_negative_effective_date_wrong_format():
@@ -385,4 +682,4 @@ def test_determine_dataset_negative_effective_date_wrong_format():
)
ws = wb['Data']
with pytest.raises(ClientError):
- determine_dataset(ws, 'BAT-000-000-000')
+ _determine_dataset(ws, 'BAT-000-000-000')
diff --git a/tests/test_tasks_manager.py b/tests/test_tasks_manager.py
index 4f7e4c9..6c8b707 100644
--- a/tests/test_tasks_manager.py
+++ b/tests/test_tasks_manager.py
@@ -5,6 +5,7 @@
from unittest.mock import patch
+from connect.client import ClientError
from sqlalchemy import null
from connect_ext_ppr.models.deployment import Deployment, DeploymentRequest
@@ -15,16 +16,18 @@
TasksStatusChoices,
TaskTypesChoices,
)
-from connect_ext_ppr.client.exception import ClientError
+from connect_ext_ppr.client.exception import CBCClientError
from connect_ext_ppr.models.task import Task
from connect_ext_ppr.tasks_manager import (
_check_cbc_task_status,
_send_ppr,
apply_ppr_and_delegate_to_marketplaces,
+ apply_pricelist_task,
check_and_update_product,
delegate_to_l2,
main_process,
TaskException,
+ validate_pricelists_task,
)
from connect_ext_ppr.services.cbc_hub import CBCService
@@ -44,7 +47,7 @@ def test__send_ppr(parse_ppr_success_response, sample_ppr_file, mocker):
def test__send_ppr_max_retries(sample_ppr_file, mocker):
cbc_service = mocker.Mock()
- cbc_service.parse_ppr.side_effect = ClientError('Some random error')
+ cbc_service.parse_ppr.side_effect = CBCClientError('Some random error')
with pytest.raises(TaskException) as ex:
_send_ppr(cbc_service, sample_ppr_file)
assert str(ex) == 'Some random error'
@@ -66,7 +69,7 @@ def test__check_cbc_task_status_with_max_retries(task_logs_response, mocker):
not_started_log = copy.deepcopy(task_logs_response)
not_started_log[0]['status'] = CBCTaskLogStatus.not_started
cbc_service = mocker.Mock()
- cbc_service.search_task_logs_by_name.side_effect = ClientError('Some random error')
+ cbc_service.search_task_logs_by_name.side_effect = CBCClientError('Some random error')
with mocker.patch('connect_ext_ppr.tasks_manager.time.sleep', return_value=None):
with pytest.raises(TaskException) as ex:
_check_cbc_task_status(cbc_service, 100)
@@ -505,6 +508,27 @@ def mock_get(key):
).count() == pending_tasks
+def test_main_process_wo_hub_credentials(
+ deployment_factory,
+ deployment_request_factory,
+ ppr_version_factory,
+ task_factory,
+ connect_client,
+ mocker,
+):
+ dep = deployment_factory()
+ ppr = ppr_version_factory(id='PPR-123', product_version=1, deployment=dep, version=1)
+ dr = deployment_request_factory(deployment=dep, delegate_l2=True, ppr=ppr)
+ task = task_factory(
+ deployment_request=dr, task_index='0001', type=TaskTypesChoices.product_setup,
+ )
+ mocker.patch('connect_ext_ppr.client.utils.get_hub_credentials', return_value=None)
+ assert main_process(dr.id, {}, connect_client) == DeploymentRequestStatusChoices.error
+
+ assert task.status == TasksStatusChoices.error
+ assert task.error_message == 'Hub Credentials not found for Hub ID HB-0000-0000.'
+
+
@patch.object(CBCService, '__init__', return_value=None)
@pytest.mark.parametrize(
('task_statuses', 'done_tasks', 'aborted_tasks'),
@@ -720,3 +744,212 @@ def mock_get(key):
Task.started_at.is_(null()),
Task.finished_at.is_(null()),
).count() == pending_tasks
+
+
+def test_validate_pricelists_task_ok(
+ deployment_factory,
+ deployment_request_factory,
+ marketplace_config_factory,
+ mocker,
+):
+ validate_mock = mocker.patch(
+ 'connect_ext_ppr.tasks_manager.validate_pricelist_batch',
+ )
+
+ dep = deployment_factory()
+ dep_req = deployment_request_factory(deployment=dep)
+
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-1', pricelist_id='BAT-1',
+ )
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-2', pricelist_id=None,
+ )
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-3', pricelist_id='BAT-OLD',
+ )
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-4', pricelist_id=None,
+ )
+
+ # already applied. skip
+ marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-1', pricelist_id='BAT-1',
+ )
+ # price list is not set. skip
+ marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-2', pricelist_id=None,
+ )
+ marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-3', pricelist_id='BAT-3',
+ )
+ marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-4', pricelist_id='BAT-4',
+ )
+
+ validate_pricelists_task(dep_req, mocker.MagicMock())
+
+ assert validate_mock.call_count == 2
+ assert {
+ validate_mock.call_args_list[0][0][1],
+ validate_mock.call_args_list[1][0][1],
+ } == {'BAT-3', 'BAT-4'}
+
+
+def test_validate_pricelists_task_validation_failed(
+ deployment_factory,
+ deployment_request_factory,
+ marketplace_config_factory,
+ mocker,
+):
+ validate_mock = mocker.patch(
+ 'connect_ext_ppr.tasks_manager.validate_pricelist_batch',
+ side_effect=ClientError(message='Not valid.', status_code='VAL-001'),
+ )
+
+ dep = deployment_factory()
+ dep_req = deployment_request_factory(deployment=dep)
+
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-3', pricelist_id='BAT-OLD',
+ )
+ marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-4', pricelist_id=None,
+ )
+
+ marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-3', pricelist_id='BAT-3',
+ )
+ marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-4', pricelist_id='BAT-4',
+ )
+
+ with pytest.raises(TaskException) as e:
+ validate_pricelists_task(dep_req, mocker.MagicMock())
+
+ assert str(e.value) == (
+ 'Price list BAT-3 of marketplace MP-3 validation failed: Not valid.'
+ )
+
+ assert validate_mock.call_count == 1
+
+
+def test_apply_pricelist_task_ok(
+ deployment_factory,
+ deployment_request_factory,
+ marketplace_config_factory,
+ mocker,
+ dbsession,
+):
+ apply_mock = mocker.patch(
+ 'connect_ext_ppr.tasks_manager.apply_pricelist_to_marketplace',
+ )
+
+ dep = deployment_factory()
+ dep_req = deployment_request_factory(deployment=dep)
+
+ dep_mp = marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-1', pricelist_id='BAT-0',
+ )
+ req_mp = marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-1', pricelist_id='BAT-1',
+ )
+
+ cbc_service = mocker.MagicMock()
+ connect_client = mocker.MagicMock()
+
+ apply_pricelist_task(
+ dep_req,
+ cbc_service,
+ connect_client,
+ req_mp,
+ dbsession,
+ )
+
+ assert apply_mock.called_once_with(
+ dep_req,
+ cbc_service,
+ connect_client,
+ req_mp,
+ )
+
+ dbsession.refresh(dep_mp)
+
+ assert dep_mp.pricelist_id == req_mp.pricelist_id
+
+
+def test_apply_pricelist_task_ok_manual(
+ deployment_factory,
+ deployment_request_factory,
+ marketplace_config_factory,
+ mocker,
+ dbsession,
+):
+ apply_mock = mocker.patch(
+ 'connect_ext_ppr.tasks_manager.apply_pricelist_to_marketplace',
+ )
+
+ dep = deployment_factory()
+ dep_req = deployment_request_factory(deployment=dep, manually=True)
+
+ dep_mp = marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-1', pricelist_id='BAT-0',
+ )
+ req_mp = marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-1', pricelist_id='BAT-1',
+ )
+
+ cbc_service = mocker.MagicMock()
+ connect_client = mocker.MagicMock()
+
+ apply_pricelist_task(
+ dep_req,
+ cbc_service,
+ connect_client,
+ req_mp,
+ dbsession,
+ )
+
+ assert apply_mock.call_count == 0
+ assert dep_mp.pricelist_id == req_mp.pricelist_id
+
+
+def test_apply_pricelist_task_error(
+ deployment_factory,
+ deployment_request_factory,
+ marketplace_config_factory,
+ mocker,
+ dbsession,
+):
+ mocker.patch(
+ 'connect_ext_ppr.tasks_manager.apply_pricelist_to_marketplace',
+ side_effect=ClientError(
+ message='olala',
+ status_code='VAL-1',
+ ),
+ )
+
+ dep = deployment_factory()
+ dep_req = deployment_request_factory(deployment=dep)
+
+ dep_mp = marketplace_config_factory(
+ deployment=dep, marketplace_id='MP-1', pricelist_id='BAT-0',
+ )
+ req_mp = marketplace_config_factory(
+ deployment_request=dep_req, marketplace_id='MP-1', pricelist_id='BAT-1',
+ )
+
+ cbc_service = mocker.MagicMock()
+ connect_client = mocker.MagicMock()
+
+ with pytest.raises(TaskException) as e:
+ apply_pricelist_task(
+ dep_req,
+ cbc_service,
+ connect_client,
+ req_mp,
+ dbsession,
+ )
+
+ assert str(e.value) == 'Error while processing pricelist: olala'
+ assert dep_mp.pricelist_id == 'BAT-0'