Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
lib-py-b2b / fedex_api.py
Size: Mime:
from os import environ
from datetime import datetime
from dateutil.parser import isoparse
from typing import Optional

from .carrier import CarrierIntegration
from .address import Address, AddressValidationResult, ValidationAddressType
from .fulfillment import Fulfillment
from .fulfillment_status import FulfillmentStatus
from .errors import LabelPrintError, AddressValidationServiceError
from .orders import Orders
from .print import PrintRequest, Printer
from .fedex import FedexServiceType, FedexSignatureOptionType, FedexSpecialServiceType, \
    FedexPhysicalPackaging, FedexLabelPrintOrientation, FedexLabelStockType, \
    FedexLabelImageType, FedexLabelType, FedexPaymentType, FedexPackaging, \
    FedexDropOffType, FedexCustomerReferenceType, FedexTrackingIdType, FedexTransitTime
from fedex.services.ship_service import FedexProcessShipmentRequest, FedexDeleteShipmentRequest
from fedex.services.rate_service import FedexRateServiceRequest
from fedex.services.track_service import FedexTrackRequest
from fedex.base_service import FedexError, FedexFailure
from fedex.config import FedexConfig
from fedex.services.address_validation_service import FedexAddressValidationRequest
from fedex.services.availability_commitment_service import FedexAvailabilityCommitmentRequest
from fedex.tools.conversion import basic_sobject_to_dict
import logging
from aws_xray_sdk.core import xray_recorder

log_level = logging.getLevelName(environ['LOG_LEVEL']) if 'LOG_LEVEL' in environ else logging.INFO
logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
                    datefmt='%d-%m-%Y:%H:%M:%S',
                    level=log_level)
logger = logging.getLogger('lib-b2b-fedex_api')


"""

FedEx Test System Access Confirmation


	Contact Info		License Authorization		Confirmation	
Congratulations! Your Test System Access information is confirmed.
There are two parts to the FedEx Authentication process. There is a security code and a
test key.
Please retain the following information in a secured environment. You will need
this information to run your Web Services in the FedEx test environment.
A confirmation email will be sent to you with your Test Security Code and directions for
testing your FedEx Web Service application in the FedEx Test environment.
Required for All Web Services
Developer Test Key:	 ehwbQ1vaYKdewC1b
Required for FedEx Web Services for Intra Country Shipping in US and Global
Test Account Number:	 510087500
Test	Meter Number:	 119116198
Required for FedEx Web Services for Office and Print
Test	FedEx Office Integrator ID:	 123
Test	Client Product ID:	 TEST
Test	Client Product Version:	 9999
"""

"""
case id: 47110201
Account No: 678421081
Supported Web Services:	 FedEx Web Services for Shipping
Authentication Key:	 9Bf2qvscdTbiLYxf
Meter Number:	 114350995
Production Account Information
Production URL: https://ws.fedex.com:443/web-services 
Password: 4WMSKaSnZGz0YVBYbR4QlHOQQ 
FedEx Shipping Account Number: XXXXXX081 
FedEx Web Services Meter Number: 114350995 
"""



"""
Case id: 47110787
Account No: 105970960
Supported Web Services:	 FedEx Web Services for Shipping
Authentication Key:	 bL1AC5ALnaX7AXqK
Meter Number:	 114352627
Production URL: https://ws.fedex.com:443/web-services 
Password: b3GArrgYwbkZxerBXbh8YJBGR 
FedEx Shipping Account Number: XXXXXX960 
FedEx Web Services Meter Number: 114352627 
"""

"""
Case id: 47110663
Account No: 898601919
Supported Web Services:	 FedEx Web Services for Shipping
Authentication Key:	 SunYT31WTo22lkOM
Meter Number:	 114352689
Production URL: https://ws.fedex.com:443/web-services 
Password: YjIwFY68cOpJu46jodljoxnGf 
FedEx Shipping Account Number: XXXXXX919 
FedEx Web Services Meter Number: 114352689 
"""


class FedexAPI(CarrierIntegration):
    def __init__(self, profile):
        super().__init__(profile)
        self.fedex_config = FedexConfig(key=self.profile.fulfillment_config.fedex_api_config.key,
                                        password=self.profile.fulfillment_config.fedex_api_config.password,
                                        account_number=self.profile.fulfillment_config.fedex_api_config.account_number,
                                        meter_number=self.profile.fulfillment_config.fedex_api_config.meter_number,
                                        use_test_server=self.profile.fulfillment_config.fedex_api_config.use_test_mode)

    @xray_recorder.capture()
    def track_shipment(self, fulfillment):
        results = []
        _fulfillment = Fulfillment.for_(fulfillment)
        if 'tracking_numbers' in _fulfillment:
            for tn in _fulfillment['tracking_numbers']:
                track = FedexTrackRequest(self.fedex_config)
                track.SelectionDetails.PackageIdentifier.Type = 'TRACKING_NUMBER_OR_DOORTAG'
                track.SelectionDetails.PackageIdentifier.Value = tn
                track.send_request()
                from fedex.tools.conversion import sobject_to_dict
                results.append(sobject_to_dict(track.response))
                from fedex.tools.conversion import sobject_to_json
                print(sobject_to_json(track.response))
        return results

    @xray_recorder.capture()
    def get_shipping_rates(self, fulfillment):
        rate = FedexRateServiceRequest(self.fedex_config)
        _fulfillment = Fulfillment.for_(fulfillment)
        recipient_address = Orders.for_(_fulfillment['order_id']).ship_to
        rate.RequestedShipment.DropoffType = FedexDropOffType.REQUEST_COURIER.value if self.profile.fulfillment_config.fedex_api_config.request_pickup else FedexDropOffType.REGULAR_PICKUP.value
        rate.RequestedShipment.ServiceType = FedexServiceType(_fulfillment.get('service_type', 92)).name
        rate.RequestedShipment.PackagingType = FedexPackaging.YOUR_PACKAGING.value
        sender = self.profile.sender_address
        rate.RequestedShipment.Shipper = self.__convert_to_fedex_address(address=sender, fedex_party=rate.RequestedShipment.Shipper)
        rate.RequestedShipment.Recipient = self.__convert_to_fedex_address(address=recipient_address, fedex_party=rate.RequestedShipment.Recipient)
        rate.RequestedShipment.EdtRequestType = 'NONE'
        rate.RequestedShipment.ShippingChargesPayment.Payor.ResponsibleParty.AccountNumber = self.profile.fulfillment_config.fedex_api_config.account_number
        rate.RequestedShipment.ShippingChargesPayment.PaymentType = FedexPaymentType.SENDER.value
        weight = rate.create_wsdl_object_of_type('Weight')
        weight.Value = _fulfillment.get('weight', 0)
        weight.Units = self.profile.fulfillment_config.get_weight_type(_fulfillment.get('weight_um', 'LB')).name
        package_line_item = rate.create_wsdl_object_of_type('RequestedPackageLineItem')
        package_line_item.PhysicalPackaging = FedexPhysicalPackaging(
            self.profile.fulfillment_config.fedex_api_config.physical_packaging).value if self.profile.fulfillment_config.fedex_api_config.physical_packaging else FedexPhysicalPackaging.BOX.value
        package_line_item.Weight = weight
        package_line_item.GroupPackageCount = 1
        rate.add_package(package_line_item)
        rate.send_request()
        from fedex.tools.conversion import sobject_to_dict
        response_dict = sobject_to_dict(rate.response)
        return response_dict['RateReplyDetails'][0]

    @xray_recorder.capture()
    def remove_shipment(self, fulfillment):
        _fulfillment = Fulfillment.for_(fulfillment)
        if 'tracking_numbers' in _fulfillment:
            for tn in _fulfillment['tracking_numbers']:
                del_shipment = FedexDeleteShipmentRequest(self.fedex_config)
                del_shipment.DeletionControlType = "DELETE_ALL_PACKAGES"
                del_shipment.TrackingId.TrackingNumber = tn
                del_shipment.TrackingId.TrackingIdType = FedexTrackingIdType.for_(FedexServiceType(_fulfillment.get('service_type', 92)))
                del_shipment.send_request()
                if del_shipment.response.HighestSeverity == "SUCCESS":
                    logger.info(f"Successfully deleted fedex shipment for fulfillment {_fulfillment['id']}")
                logger.debug(del_shipment.response)

    @xray_recorder.capture()
    def generate_label_image(self, fulfillment):
        _fulfillment = Fulfillment.for_(fulfillment)
        if 'labels' in _fulfillment:
            label_images = self.__convert_label_to_png(_fulfillment['id'], _fulfillment['labels'])
            return label_images
        else:
            raise LabelPrintError(
                f"Unable to convert labels because they don't exist on fulfillment {_fulfillment['id']}")

    @xray_recorder.capture()
    def __convert_label_to_png(self, fullfillment_id, labels):
        """
        I'm a little nervoud about sending this data to this online service.  Would like to find a better way to generate an image for ZPL.
        :param fullfillment_id:
        :param labels:
        :return:
        """
        import requests
        import base64
        label_images = []
        for label in labels:
            for image in label['parts']:
                zpl = base64.b64decode(image)

                # adjust print density (8dpmm), label width (4 inches), label height (6 inches), and label index (0) as necessary
                url = 'http://api.labelary.com/v1/printers/12dpmm/labels/4x6/0/'
                files = {'file': zpl}
                # headers = {'Accept': 'application/pdf'}  # omit this line to get PNG images back
                response = requests.post(url, files=files)

                if response.status_code == 200:
                    response.raw.decode_content = True
                    encoded_image = base64.b64encode(response.content)
                    label_images.append(encoded_image)
                else:
                    logger.error(response.text)
        return label_images

    @xray_recorder.capture()
    def __convert_to_fedex_address(self, address: Address, fedex_party):
        fedex_party.Address.StreetLines = []
        fedex_party.Address.StreetLines.append(address.address1)
        if address.address2:
            fedex_party.Address.StreetLines.append(address.address2)
        fedex_party.Address.City = address.city
        fedex_party.Address.StateOrProvinceCode = address.state
        fedex_party.Address.PostalCode = address.postalCode
        fedex_party.Address.CountryCode = address.country
        fedex_party.Address.Residential = False
        if address.company:
            fedex_party.Contact.CompanyName = address.company
        else:
            fedex_party.Contact.PersonName = address.name
        fedex_party.Contact.PhoneNumber = address.phone
        return fedex_party

    @xray_recorder.capture()
    def __convert_from_fedex_address(self, address_dict: dict) -> Address:
        street_lines = address_dict.get('StreetLines', [])
        address1 = street_lines[0] if len(street_lines) > 0 else None
        address2 = street_lines[1] if len(street_lines) > 1 else None
        _address = Address(address1=address1,
                           address2=address2,
                           city=address_dict.get('City', None),
                           state=address_dict.get('StateOrProvinceCode', None),
                           postalCode=address_dict.get('PostalCode', None),
                           country=address_dict.get('CountryCode', None),
                           company=address_dict.get('CompanyName', None),
                           phone=address_dict.get('PhoneNumber', None),
                           name=address_dict.get('PersonName', None))
        return _address

    @xray_recorder.capture()
    def validate_address(self, address: Address):
        avs_request = FedexAddressValidationRequest(self.fedex_config)
        address1 = self.__convert_to_fedex_address(address, avs_request.create_wsdl_object_of_type('AddressToValidate'))
        avs_request.add_address(address1)
        try:
            avs_request.send_request()
        except (FedexError, FedexFailure) as fe:
            logger.error("Problem validating address with Fedex API. " + str(fe))
            raise AddressValidationServiceError(str(fe)) from fe

        address_results = avs_request.response.AddressResults[0]
        attributes = {k.Name: True if k.Value == 'true' else False for k in address_results.Attributes}
        fedex_transit_time = self.get_estimated_transit_time(address)
        effective_address = self.__convert_from_fedex_address(basic_sobject_to_dict(address_results.EffectiveAddress))
        return AddressValidationResult(
            original_address=address,
            effective_address=effective_address,
            address_type=ValidationAddressType(address_results.State),
            residential=address_results.Classification == 'RESIDENTIAL',
            attributes=attributes,
            estimated_transit_time=fedex_transit_time.value
        )

    def get_estimated_transit_time(self, address: Address) -> Optional[FedexTransitTime]:
        acr_request = FedexAvailabilityCommitmentRequest(self.fedex_config)
        sender: Address = self.profile.sender_address
        acr_request.Origin.PostalCode = sender.postalCode
        acr_request.Origin.CountryCode = "US" if sender.country == "USA" else sender.country
        acr_request.Destination.PostalCode = address.postalCode
        acr_request.Destination.CountryCode = "US" if address.country == "USA" else address.country
        acr_request.CarrierCode = 'FDXG'
        try:
            acr_request.send_request()
        except (FedexError, FedexFailure) as fe:
            logger.error("Problem checking service availability for address with Fedex API. " + str(fe))
            raise AddressValidationServiceError(str(fe)) from fe
        if acr_request.response.Options:
            _options = {FedexServiceType.for_(o.Service):FedexTransitTime.for_(o.TransitTime) for o in acr_request.response.Options}
            return _options[FedexServiceType.FEDEX_GROUND]
        else:
            return None


    @xray_recorder.capture()
    def create_shipment(self, fulfillment, validate_address=False, validate_request=False):
        _fulfillment = Fulfillment.for_(fulfillment)
        recipient_address = Orders.for_(_fulfillment['order_id']).ship_to
        xray_recorder.begin_subsegment('validate_address')
        _profile_validate_address = self.profile.fulfillment_config.fedex_api_config.validate_address
        effective_address = recipient_address
        is_residential = True
        # Moving this to earlier in the process, no longer makes sense to do the validation at this step
        # if validate_address or _profile_validate_address:
        #     try:
        #         validation_result = self.validate_address(recipient_address)
        #         effective_address = validation_result.effective_address
        #         is_residential = validation_result.residential
        #         # merge in any attributes that don't come back from the address validation.
        #         effective_address.merge(recipient_address)
        #     except AddressValidationServiceError as avse:
        #         logger.error(avse)
        xray_recorder.end_subsegment()

        xray_recorder.begin_subsegment('create_api_request')
        # shipment_uid = f"{order.get_shipment_id()}-{i}"
        # shipment = FedexProcessShipmentRequest(self.fedex_config, customer_transaction_id=shipment_uid)
        shipment = FedexProcessShipmentRequest(self.fedex_config)

        # MARK: -- Create Requested Shipment --
        shipment.RequestedShipment.DropoffType = FedexDropOffType.REQUEST_COURIER.value if self.profile.fulfillment_config.fedex_api_config.request_pickup else FedexDropOffType.REGULAR_PICKUP.value
        shipment.RequestedShipment.ServiceType = FedexServiceType(_fulfillment.get('service_type', 92)).name
        shipment.RequestedShipment.PackagingType = FedexPackaging.YOUR_PACKAGING.value
        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('set_addresses')
        sender = self.profile.sender_address
        shipment.RequestedShipment.Shipper = self.__convert_to_fedex_address(address=sender,
                                                                             fedex_party=shipment.RequestedShipment.Shipper)
        shipment.RequestedShipment.Recipient = self.__convert_to_fedex_address(address=effective_address,
                                                                               fedex_party=shipment.RequestedShipment.Recipient)
        # This is needed to ensure an accurate rate quote with the response.
        # Use AddressValidation to get ResidentialStatus
        shipment.RequestedShipment.Recipient.Address.Residential = is_residential
        # Include Estimated Duties and Taxes for International Shipments
        shipment.RequestedShipment.EdtRequestType = 'NONE'
        xray_recorder.end_subsegment()
        # Senders account information
        xray_recorder.begin_subsegment('configure_request')
        shipment.RequestedShipment.ShippingChargesPayment.Payor.ResponsibleParty.AccountNumber = self.profile.fulfillment_config.fedex_api_config.account_number
        shipment.RequestedShipment.ShippingChargesPayment.PaymentType = FedexPaymentType.SENDER.value

        shipment.RequestedShipment.LabelSpecification.LabelFormatType = FedexLabelType.COMMON2D.value
        shipment.RequestedShipment.LabelSpecification.ImageType = FedexLabelImageType.ZPLII.value

        # To use doctab stocks, you must change ImageType above to one of the label printer formats (ZPLII, EPL2, DPL).
        shipment.RequestedShipment.LabelSpecification.LabelStockType = FedexLabelStockType(
            self.profile.fulfillment_config.fedex_api_config.label_type).value if self.profile.fulfillment_config.fedex_api_config.label_type else FedexLabelStockType.STOCK_4X6.value

        # Timestamp in YYYY-MM-DDThh:mm:ss format, e.g. 2002-05-30T09:00:00
        ship_date = _fulfillment['ship_date'] if 'ship_date' in _fulfillment else datetime.now().isoformat()
        iso_ship_date = isoparse(ship_date).replace(microsecond=0).isoformat()
        shipment.RequestedShipment.ShipTimestamp = iso_ship_date

        shipment.RequestedShipment.LabelSpecification.LabelPrintingOrientation = FedexLabelPrintOrientation(
            self.profile.fulfillment_config.fedex_api_config.label_orientation).value if self.profile.fulfillment_config.fedex_api_config.label_orientation else FedexLabelPrintOrientation.BOTTOM_EDGE_OF_TEXT_FIRST.value

        # Delete the flags we don't want.
        if hasattr(shipment.RequestedShipment.LabelSpecification, 'LabelOrder'):
            del shipment.RequestedShipment.LabelSpecification.LabelOrder  # Delete, not using.

        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('create_package_line')
        # Create Weight
        weight = shipment.create_wsdl_object_of_type('Weight')
        weight.Value = float(_fulfillment.get('weight', 0))
        weight.Units = self.profile.fulfillment_config.get_weight_type(_fulfillment.get('weight_um', 'LB')).name

        # Create PackageLineItem
        package_line_item = shipment.create_wsdl_object_of_type('RequestedPackageLineItem')
        # BAG, BARREL, BASKET, BOX, BUCKET, BUNDLE, CARTON, CASE, CONTAINER, ENVELOPE etc..
        package_line_item.PhysicalPackaging = FedexPhysicalPackaging(
            self.profile.fulfillment_config.fedex_api_config.physical_packaging).value if self.profile.fulfillment_config.fedex_api_config.physical_packaging else FedexPhysicalPackaging.BOX.value
        package_line_item.Weight = weight

        # Insured Value
        # insure = shipment.create_wsdl_object_of_type('Money')
        # insure.Currency = 'USD'
        # insure.Amount = 1.0
        # Add Insured and Total Insured values.
        # package_line_item.InsuredValue = insure
        # shipment.RequestedShipment.TotalInsuredValue = insure
        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('configure_references')
        # Add customer reference
        customer_reference = shipment.create_wsdl_object_of_type('CustomerReference')
        customer_reference.CustomerReferenceType = "CUSTOMER_REFERENCE"
        customer_reference.Value = _fulfillment.get('container_id', 'Unknown Container')
        package_line_item.CustomerReferences.append(customer_reference)

        # Add department number
        # department_number = shipment.create_wsdl_object_of_type('CustomerReference')
        # department_number.CustomerReferenceType = FedexCustomerReferenceType.DEPARTMENT_NUMBER.value
        # department_number.Value = self.profile.department
        # package_line_item.CustomerReferences.append(department_number)

        # Add invoice number
        if 'glovia_order' in _fulfillment:
            invoice_number = shipment.create_wsdl_object_of_type('CustomerReference')
            invoice_number.CustomerReferenceType = FedexCustomerReferenceType.INVOICE_NUMBER.value
            invoice_number.Value = _fulfillment.get('glovia_order', 'Unknown Orders')
            package_line_item.CustomerReferences.append(invoice_number)

        # Add po number
        po_number = shipment.create_wsdl_object_of_type('CustomerReference')
        po_number.CustomerReferenceType = FedexCustomerReferenceType.P_O_NUMBER.value
        po_number.Value = _fulfillment.get('purchase_order', 'Unknown PO')
        package_line_item.CustomerReferences.append(po_number)

        if 'signature_required' in _fulfillment:
            package_line_item.SpecialServicesRequested.SpecialServiceTypes = FedexSpecialServiceType.SIGNATURE_OPTION.value
            package_line_item.SpecialServicesRequested.SignatureOptionDetail.OptionType = FedexSignatureOptionType.SERVICE_DEFAULT.value

        # This adds the RequestedPackageLineItem WSDL object to the shipment. It
        # increments the package count and total weight of the shipment for you.
        shipment.add_package(package_line_item)
        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('send_request')
        logger.debug("RequestedShipment...")
        logger.debug(shipment.RequestedShipment)
        logger.debug("ClientDetail...")
        logger.debug(shipment.ClientDetail)
        logger.debug("TransactionDetail...")
        logger.debug(shipment.TransactionDetail)
        _profile_validate_request = self.profile.fulfillment_config.fedex_api_config.verify_request
        if validate_request or _profile_validate_request:
            xray_recorder.begin_subsegment('validate_request')
            shipment.send_validation_request()
            xray_recorder.end_subsegment()
            if shipment.response.HighestSeverity == "SUCCESS":
                shipment.send_request()
        else:
            shipment.send_request()

        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('process_response')
        logger.debug("Shipment Response...")
        logger.debug(shipment.response)


        from fedex.tools.conversion import sobject_to_dict
        response_dict = sobject_to_dict(shipment.response)
        # response_dict['CompletedShipmentDetail']['CompletedPackageDetails'][0]['Label']['Parts'][0]['Image'] = ''
        # This will dump the response data dict to json.
        # from fedex.tools.conversion import sobject_to_json
        # response_json = sobject_to_json(shipment.response)
        logger.debug(f"HighestSeverity: {shipment.response.HighestSeverity}")

        shipment_data = {
            'tracking_numbers': [],
            'carrier_code': '',
            'carrier_data': response_dict,
            'transit_time': '',
            'labels': [],
            'net_shipping_cost': 0.00,
            'saturday_delivery': False
        }
        # NOTE: -------------------------------------------------------
        # This is built on the assumption that we do not have
        # multi part shipments.  One Shipment = One Box.
        # -------------------------------------------------------------
        csd = response_dict['CompletedShipmentDetail']
        shipment_data['transit_time'] = csd['OperationalDetail']['TransitTime']
        shipment_data['carrier_code'] = csd['CarrierCode']
        if 'OperationalDetail' in csd and 'DeliveryEligibilities' in csd['OperationalDetail']:
            shipment_data['saturday_delivery'] = (
                        'SATURDAY_DELIVERY' in csd['OperationalDetail']['DeliveryEligibilities'])
        for cpd in csd['CompletedPackageDetails']:
            for tid in cpd['TrackingIds']:
                shipment_data['tracking_numbers'].append(tid['TrackingNumber'])
            label = cpd['Label']
            shipment_data['labels'].append({
                'copies': label['CopiesToPrint'],
                'parts': [i['Image'] for i in label['Parts']]
            })
        if 'ShipmentRating' in csd and 'ShipmentRateDetails' in csd['ShipmentRating']:
            for srd in csd.get('ShipmentRating', {}).get('ShipmentRateDetails', []):
                shipment_data['ship_zone'] = srd.get('RatedZone')
                shipment_data['net_shipping_cost'] += srd.get('TotalNetCharge', {}).get('Amount', 0.0)

        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('update_fulfillment')
        Fulfillment.update_for_shipment(fulfillment=_fulfillment['id'],
                                        status=FulfillmentStatus.NOT_SENT,
                                        tracking_numbers=shipment_data['tracking_numbers'],
                                        ship_dates=[iso_ship_date],
                                        net_shipping_cost=shipment_data['net_shipping_cost'],
                                        labels=shipment_data['labels'],
                                        carrier_code=shipment_data['carrier_code'],
                                        carrier_data=shipment_data['carrier_data'],
                                        transit_time=shipment_data['transit_time']
                                        )

        xray_recorder.end_subsegment()
        xray_recorder.begin_subsegment('create_print_request')
        if self.profile.fulfillment_config.fedex_api_config.print_immediately:
            print_request = PrintRequest.create(printer=Printer.for_(_fulfillment['customer_edi_id']),
                                                fulfillment=_fulfillment)
            logger.info(f"Requested print for fulfillment {_fulfillment['id']} with request {print_request}")
        xray_recorder.end_subsegment()