"""Timed Release Model CRUD operation.""" from datetime import datetime, timezone from dateutil import parser from oto import response from oto import status as http_status from sentry_sdk import capture_exception from sqlalchemy import Column, DateTime, Enum, Integer, String from timed_release import config from timed_release.connectors import sql from timed_release.constants import error from timed_release.constants.field_const import STAGGERED from timed_release.constants.timed_release import ( OA, SOURCE, TIMED, WORKSTATION, ) class TimedRelease(sql.base_model): """Table definition for timed_release table.""" __tablename__ = 'timed_release' timed_release_id = Column( 'timed_release_id', Integer, primary_key=True, nullable=False) product_id = Column( 'product_id', Integer, nullable=False) timing = Column(Enum(*config.TIMING), nullable=False) store_id = Column( 'store_id', Integer, nullable=False) sales_date_time = Column(DateTime, nullable=False) created_on = Column(DateTime, nullable=False) created_by = Column(String(127)) updated_on = Column(DateTime, nullable=False) updated_by = Column(String(127)) source = Column(Enum(*SOURCE), nullable=False) user_timezone = Column(String(50)) def to_dict(self): """Return a dictionary of a timed_release.""" return { 'sale_date_time': self.sales_date_time, 'store_id': self.store_id, 'timing': self.timing } def to_ws_dict(self): """Return a dictionary of a timed_release for workstation.""" if isinstance(self.sales_date_time, datetime): sales_date_time = self.sales_date_time.strftime( '%Y-%m-%dT%H:%M:%SZ') else: sales_date_time = self.sales_date_time return { 'sales_date_time': sales_date_time, 'store_id': self.store_id, 'timing': self.timing, 'source': self.source, 'user_timezone': self.user_timezone } @sql.wrap_db_errors def get_product_timed_release_data(product_id): """Get all information of product timed release for given product id. Args: product_id (int): Product id to fetch product timed release details. Return: response: message containing data upon successful query. error Response message otherwise. """ with sql.db_session() as session: result = session.query(TimedRelease).filter( TimedRelease.product_id == product_id ).all() staggered = [] timed = [] if result: for result_row in result: timed_release_row = result_row.to_dict() sales_date_time = timed_release_row['sale_date_time'] if timed_release_row['timing'] == 'staggered': staggered.append({ 'sale_date': sales_date_time.strftime('%Y-%m-%d'), 'delivery_store': { 'id': timed_release_row['store_id'] } }) else: timed.append({ 'sale_date_time': sales_date_time.strftime( '%Y-%m-%dT%H:%M:%SZ'), 'delivery_store': { 'id': timed_release_row['store_id'] } }) return response.Response( message={'timed': timed, 'staggered': staggered}) @sql.wrap_db_errors def update_product_timed_release_data( product_id, data, orchard_identity_id, session=None): """Check if timed release data exists for the product and store. If data exists, update the data. If data does not exist, create new records. Args: product_id (int): Product id for which timed release data has to be updated. data (list): timed release data objects list orchard_identity_id (string): users identity id Return: response.Response: timed release data on successful update or error response. """ try: timed_release_data = get_product_timed_release_data(product_id) existing_timed_data = timed_release_data.message.get('timed') existing_staggered_data = timed_release_data.message.get('staggered') if data.get('timed') is not None: existing_timed_stores = [] if len(existing_timed_data): for single_timed_obj in existing_timed_data: existing_timed_stores.append( single_timed_obj['delivery_store']['id']) timed_stores = [] for single_timed_obj in data['timed']: store_id = single_timed_obj['delivery_store']['id'] timed_stores.append(store_id) single_timed_obj['sale_date_time'] = parser.isoparse( single_timed_obj['sale_date_time']) if store_id in existing_timed_stores: update_timed_release_data_for_store( product_id, single_timed_obj, 'sale_date_time', orchard_identity_id, session=session) else: add_timed_release_data_for_store( product_id, single_timed_obj, 'timed', 'sale_date_time', orchard_identity_id, session=session) records_to_delete = list( set(existing_timed_stores) - set(timed_stores)) for store_id in records_to_delete: delete_timed_release_record( product_id, store_id, 'timed', session=session) if data.get('staggered') is not None: existing_staggered_stores = [] if len(existing_staggered_data): for single_timed_obj in existing_staggered_data: existing_staggered_stores.append( single_timed_obj['delivery_store']['id']) staggered_stores = [] for single_staggered_obj in data['staggered']: store_id = single_staggered_obj['delivery_store']['id'] staggered_stores.append(store_id) single_staggered_obj['sale_date'] = parser.isoparse( single_staggered_obj['sale_date']) if store_id in existing_staggered_stores: update_timed_release_data_for_store( product_id, single_staggered_obj, 'sale_date', orchard_identity_id, session=session) else: add_timed_release_data_for_store( product_id, single_staggered_obj, 'staggered', 'sale_date', orchard_identity_id, session=session) records_to_delete = list( set(existing_staggered_stores) - set(staggered_stores)) for store_id in records_to_delete: delete_timed_release_record( product_id, store_id, 'staggered', session=session) return get_product_timed_release_data(product_id) except Exception: capture_exception() return response.create_error_response( status=500, code=error.INTERNAL_ERROR, message='Error while updating timed release data') @sql.db_session_wrap @sql.wrap_db_errors def update_timed_release_data_for_store(product_id, data, key, orchard_identity_id, session=None): """Update timed release data for the store. Args: product_id (int): Product id for which timed release data has to be updated. data (object): timed release data object orchard_identity_id (string): users identity id """ timed_release_obj = session.query(TimedRelease).filter_by( product_id=product_id, store_id=data['delivery_store']['id']).first() timed_release_obj.sales_date_time = data[key] timed_release_obj.updated_on = datetime.now(timezone.utc) timed_release_obj.updated_by = orchard_identity_id timed_release_obj.source = OA @sql.db_session_wrap @sql.wrap_db_errors def add_timed_release_data_for_store(product_id, data, timing, key, orchard_identity_id, session=None): """Add timed release data for the store. Args: product_id (int): Product id for which timed release data has to be updated. data (object): timed release data object timing (string): whether it is timed or staggered orchard_identity_id (string): users identity id """ timed_release_obj = TimedRelease( product_id=product_id, timing=timing, store_id=data['delivery_store']['id'], sales_date_time=data[key], created_on=datetime.now(timezone.utc), created_by=orchard_identity_id, updated_on=datetime.now(timezone.utc), updated_by=orchard_identity_id, source=OA) session.add(timed_release_obj) @sql.db_session_wrap @sql.wrap_db_errors def delete_timed_release_record(product_id, store_id, timing, session=None): """Delete timed release data for the store. Args: product_id (int): Product id for which timed release data has to be updated. store_id (int): store identifier timing (string): whether it is timed or staggered """ session.query(TimedRelease)\ .filter(TimedRelease.product_id == product_id)\ .filter(TimedRelease.store_id == store_id)\ .filter(TimedRelease.timing == timing)\ .delete() @sql.wrap_db_errors def get_supported_stores_timed_release_ws(product_id, session): """Get product's supported stores timed releases set in workstation. Args: product_id (int): Product id to fetch product timed release details. session (object): database session Return: response (object): supported stores timed release data or 404 or error. """ result = session.query(TimedRelease).filter( TimedRelease.product_id == product_id, TimedRelease.source == WORKSTATION ).all() if not result: return response.Response(status=http_status.NOT_FOUND) timed_releases = [result_row.to_ws_dict() for result_row in result] return response.Response( status=http_status.OK, message={'timed_releases': timed_releases} ) @sql.wrap_db_errors def check_if_supported_stores_timed_release_oa_exists(product_id, session): """Check if supported stores timed releases set in OA exists. Args: product_id (int): Product id to fetch product timed release details. session (object): database session Return: result (boolean): True or False. """ result = session.query(TimedRelease).filter( TimedRelease.product_id == product_id, TimedRelease.source == OA ).all() if not result: return False return True @sql.wrap_db_errors def upsert_supported_stores_timed_release_ws( product_id, supported_stores_tr_data, identity_id, session): """Upsert timed release data for supported stores for a given product. It performs the following steps: 1. Retrieve the current timed release data for the given product from the workstation source. 2. Compare the request timed release data with the existing data. 3. For each store in the request data: - If a timed release already exists for that store, update the existing record. - If no timed release exists for that store, create a new record. 4. Return a response containing all successfully upserted records. Args: product_id (int): Unique product identifier for which the timed release data must be upserted. supported_stores_tr_data (list): List of dictionaries representing timed release data for each supported store. identity_id (str): Unique identifier of the user performing the operation, used for auditing. session (object): Active database session used for read and write operations. Returns: response.Response: A response object with: - status: `http_status.OK` on success, or an appropriate error status. - message: A dictionary containing all upserted timed release records under the key `supported_releases`, or an error message if the operation failed. """ try: supported_tr_ws_response = get_supported_stores_timed_release_ws( product_id, session ) if supported_tr_ws_response.status == http_status.INTERNAL_ERROR: return supported_tr_ws_response existing_timed_releases = ( supported_tr_ws_response.message['timed_releases'] if supported_tr_ws_response.status == http_status.OK else [] ) existing_timed_stores = ( [ tr['store_id'] for tr in existing_timed_releases if existing_timed_releases ] ) upserted_timed_releases = [] for single_timed_data in supported_stores_tr_data: if single_timed_data['store_id'] in existing_timed_stores: updated_timed_release = update_timed_release_ws( product_id, single_timed_data, identity_id, session) upserted_timed_releases.append(updated_timed_release) else: added_timed_release = add_timed_release_ws( product_id, single_timed_data, identity_id, session) upserted_timed_releases.append(added_timed_release) return response.Response( status=http_status.OK, message={'supported_releases': upserted_timed_releases} ) except Exception: capture_exception() return response.create_error_response( status=http_status.OK, code=error.INTERNAL_ERROR, message=error.ERROR_MESSAGE_UPSERTING_SUPPORTED_TIMED_RELEASE ) @sql.wrap_db_errors def update_timed_release_ws( product_id, data, identity_id, session): """Update workstation timed release data for the store. Args: product_id (int): Product id for which timed release data has to be updated. data (object): timed release data object identity_id (string): users identity id session (object): database session """ timed_release_obj = session.query(TimedRelease).filter_by( product_id=product_id, store_id=data['store_id'], source=WORKSTATION ).first() timed_release_obj.sales_date_time = data['sales_date_time'] timed_release_obj.updated_on = datetime.now(timezone.utc) timed_release_obj.updated_by = identity_id timed_release_obj.user_timezone = data.get('user_timezone') return timed_release_obj.to_ws_dict() @sql.wrap_db_errors def add_timed_release_ws( product_id, data, identity_id, session): """Add workstation timed release data for the store. Args: product_id (int): Product id for which timed release data has to be added. data (object): timed release data object identity_id (string): users identity id session (object): database session """ timed_release_obj = TimedRelease( product_id=product_id, timing=TIMED, store_id=data['store_id'], sales_date_time=data['sales_date_time'], created_on=datetime.now(timezone.utc), created_by=identity_id, updated_on=datetime.now(timezone.utc), updated_by=identity_id, source=WORKSTATION, user_timezone=data.get('user_timezone') ) session.add(timed_release_obj) return timed_release_obj.to_ws_dict() @sql.wrap_db_errors def get_product_timed_release_by_store_id(product_id, store_id, session): """Get timed release details for a given product and store. This fetches timed release irrespective of the source where it was set i.e OA or workstation. The result is categorized into `timed` or `staggered` based on the `timing` column value. Args: product_id (int): ID of product whose release details are requested. store_id (int): ID of the store to filter release details. session (object): database session Returns: response.Response: - 200 OK with `message` containing a dict with `timed` or `staggered` data. - 404 NOT_FOUND if no matching record is found. """ result = session.query(TimedRelease).filter( TimedRelease.product_id == product_id, TimedRelease.store_id == store_id ).first() if not result: return response.Response(status=http_status.NOT_FOUND) timed = None staggered = None timed_release = result.to_dict() if timed_release['timing'] == STAGGERED: staggered = { 'sales_date': ( timed_release['sale_date_time'].strftime( '%Y-%m-%d') ) } else: timed = { 'sales_date_time': ( timed_release['sale_date_time'].strftime( '%Y-%m-%dT%H:%M:%SZ') ) } return response.Response( message={'timed': timed, 'staggered': staggered} ) @sql.wrap_db_errors def delete_timed_release_ws(product_id, session): """Delete all workstation set timed releases data for the product_id. If no timed releases found then return 404. Args: product_id (int): Product id for which timed release data has to be updated. session (object): database session """ deleted_count = session.query(TimedRelease)\ .filter(TimedRelease.product_id == product_id)\ .filter(TimedRelease.source == WORKSTATION)\ .delete() if deleted_count == 0: return response.Response(status=http_status.NOT_FOUND) return response.Response(status=http_status.OK)