def normalize_processor_response(self, response): # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling # and logging the exception. try: decision = Decision(response['decision'].upper()) except ValueError: decision = response['decision'] _response = UnhandledCybersourceResponse( decision=decision, duplicate_payment=(decision == Decision.error and int(response['reason_code']) == 104), partial_authorization=( 'auth_amount' in response and response['auth_amount'] and response['auth_amount'] != response['req_amount']), currency=response['req_currency'], total=Decimal(response['req_amount']), card_number=response['req_card_number'], card_type=CYBERSOURCE_CARD_TYPE_MAP.get(response['req_card_type']), transaction_id=response.get( 'transaction_id', ''), # Error Notifications do not include a transaction id. order_id=response['req_reference_number'], raw_json=self.serialize_order_completion(response), ) return _response
def handle_processor_response(self, response, basket=None): """ Handle a response (i.e., "merchant notification") from CyberSource. Arguments: response (dict): Dictionary of parameters received from the payment processor. Keyword Arguments: basket (Basket): Basket being purchased via the payment processor. Raises: UserCancelled: Indicates the user cancelled payment. TransactionDeclined: Indicates the payment was declined by the processor. GatewayError: Indicates a general error on the part of the processor. InvalidCyberSourceDecision: Indicates an unknown decision value. Known values are ACCEPT, CANCEL, DECLINE, ERROR. PartialAuthorizationError: Indicates only a portion of the requested amount was authorized. Returns: HandledProcessorResponse """ # Validate the signature if not self.is_signature_valid(response): raise InvalidSignatureError # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling # and logging the exception. decision = response['decision'].lower() if decision != 'accept': reason_code = int(response['reason_code']) if decision == 'error' and reason_code == 104: raise DuplicateReferenceNumber raise { 'cancel': UserCancelled, 'decline': TransactionDeclined, 'error': GatewayError }.get(decision, InvalidCybersourceDecision) # Raise an exception if the authorized amount differs from the requested amount. # Note (CCB): We should never reach this point in production since partial authorization is disabled # for our account, and should remain that way until we have a proper solution to allowing users to # complete authorization for the entire order. if response['auth_amount'] != response['req_amount']: raise PartialAuthorizationError currency = response['req_currency'] total = Decimal(response['req_amount']) transaction_id = response['transaction_id'] card_number = response['req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get(response['req_card_type']) return HandledProcessorResponse(transaction_id=transaction_id, total=total, currency=currency, card_number=card_number, card_type=card_type)
def handle_processor_response(self, response, basket=None): """ Handle a response (i.e., "merchant notification") from CyberSource. Arguments: response (dict): Dictionary of parameters received from the payment processor. Keyword Arguments: basket (Basket): Basket being purchased via the payment processor. Raises: UserCancelled: Indicates the user cancelled payment. TransactionDeclined: Indicates the payment was declined by the processor. GatewayError: Indicates a general error on the part of the processor. InvalidCyberSourceDecision: Indicates an unknown decision value. Known values are ACCEPT, CANCEL, DECLINE, ERROR. PartialAuthorizationError: Indicates only a portion of the requested amount was authorized. Returns: HandledProcessorResponse """ # Validate the signature if not self.is_signature_valid(response): raise InvalidSignatureError # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling # and logging the exception. decision = response['decision'].lower() if decision != 'accept': exception = { 'cancel': UserCancelled, 'decline': TransactionDeclined, 'error': GatewayError }.get(decision, InvalidCybersourceDecision) raise exception # Raise an exception if the authorized amount differs from the requested amount. # Note (CCB): We should never reach this point in production since partial authorization is disabled # for our account, and should remain that way until we have a proper solution to allowing users to # complete authorization for the entire order. if response['auth_amount'] != response['req_amount']: raise PartialAuthorizationError currency = response['req_currency'] total = Decimal(response['req_amount']) transaction_id = response['transaction_id'] card_number = response['req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get(response['req_card_type']) return HandledProcessorResponse( transaction_id=transaction_id, total=total, currency=currency, card_number=card_number, card_type=card_type )
def normalize_processor_response(self, response): decision_map = { 'AUTHORIZED': Decision.accept, 'PARTIAL_AUTHORIZED': Decision.decline, 'AUTHORIZED_PENDING_REVIEW': Decision.review, 'AUTHORIZED_RISK_DECLINED': Decision.decline, 'PENDING_AUTHENTICATION': Decision.decline, 'PENDING_REVIEW': Decision.review, 'DECLINED': Decision.decline, 'INVALID_REQUEST': Decision.error, } response_json = self.serialize_order_completion(response) if isinstance(response, ApiException): decision = decision_map.get(response_json.get('status'), response_json.get('status')) return UnhandledCybersourceResponse( decision=decision, duplicate_payment=(decision == Decision.error and response_json.get('reason') == 'DUPLICATE_REQUEST'), partial_authorization=False, currency=None, total=None, card_number=None, card_type=None, transaction_id=None, order_id=None, raw_json=response_json, ) decoded_capture_context = jwt.decode(self.capture_context['key_id'], verify=False) jwk = RSAAlgorithm.from_jwk( json.dumps(decoded_capture_context['flx']['jwk'])) decoded_payment_token = jwt.decode(self.transient_token_jwt, key=jwk, algorithms=['RS256']) decision = decision_map.get(response.status, response.status) return UnhandledCybersourceResponse( decision=decision, duplicate_payment=(decision == Decision.error and response.reason == 'DUPLICATE_REQUEST'), partial_authorization=response.status == 'PARTIAL_AUTHORIZED', currency=response.order_information.amount_details.currency, total=Decimal( response.order_information.amount_details.total_amount), card_number=decoded_payment_token['data']['number'], card_type=CYBERSOURCE_CARD_TYPE_MAP.get( response.payment_information.tokenized_card.type), transaction_id=response.processor_information.transaction_id, order_id=response.client_reference_information.code, raw_json=response_json, )
def request_apple_pay_authorization(self, basket, billing_address, payment_token): """ Authorizes an Apple Pay payment. For details on the process, see the CyberSource Simple Order API documentation at https://www.cybersource.com/developers/integration_methods/apple_pay/. Args: basket (Basket) billing_address (BillingAddress) payment_token (dict) Returns: HandledProcessorResponse Raises: GatewayError """ try: client = Client(self.soap_api_url, wsse=UsernameToken(self.merchant_id, self.transaction_key)) card_type = APPLE_PAY_CYBERSOURCE_CARD_TYPE_MAP[ payment_token['paymentMethod']['network'].lower()] bill_to = { 'firstName': billing_address.first_name, 'lastName': billing_address.last_name, 'street1': billing_address.line1, 'street2': billing_address.line2, 'city': billing_address.line4, 'state': billing_address.state, 'postalCode': billing_address.postcode, 'country': billing_address.country.iso_3166_1_a2, 'email': basket.owner.email, } purchase_totals = { 'currency': basket.currency, 'grandTotalAmount': str(basket.total_incl_tax), } encrypted_payment = { 'descriptor': 'RklEPUNPTU1PTi5BUFBMRS5JTkFQUC5QQVlNRU5U', 'data': base64.b64encode(json.dumps(payment_token['paymentData'])), 'encoding': 'Base64', } card = { 'cardType': card_type, } auth_service = { 'run': 'true', } capture_service = { 'run': 'true', } # Enable Export Compliance for SDN validation, amongst other checks. # See https://www.cybersource.com/products/fraud_management/export_compliance/ export_service = { 'run': 'true', } item = [{ 'id': index, 'productCode': line.product.get_product_class().slug, 'productName': clean_field_value(line.product.title), 'quantity': line.quantity, 'productSKU': line.stockrecord.partner_sku, 'taxAmount': str(line.line_tax), 'unitPrice': str(line.unit_price_incl_tax), } for index, line in enumerate(basket.all_lines())] response = client.service.runTransaction( merchantID=self.merchant_id, merchantReferenceCode=basket.order_number, billTo=bill_to, purchaseTotals=purchase_totals, encryptedPayment=encrypted_payment, card=card, ccAuthService=auth_service, ccCaptureService=capture_service, exportService=export_service, paymentSolution='001', item=item, ) except: msg = 'An error occurred while authorizing an Apple Pay (via CyberSource) for basket [{}]'.format( basket.id) logger.exception(msg) raise GatewayError(msg) request_id = response.requestID ppr = self.record_processor_response(serialize_object(response), transaction_id=request_id, basket=basket) if response.decision == 'ACCEPT': currency = basket.currency total = basket.total_incl_tax transaction_id = request_id return HandledProcessorResponse( transaction_id=transaction_id, total=total, currency=currency, card_number='Apple Pay', card_type=CYBERSOURCE_CARD_TYPE_MAP.get(card_type)) else: msg = ( 'CyberSource rejected an Apple Pay authorization request for basket [{basket_id}]. ' 'Complete response has been recorded in entry [{response_id}]') msg = msg.format(basket_id=basket.id, response_id=ppr.id) logger.warning(msg) raise GatewayError(msg)
def handle_processor_response(self, response, basket=None): """ Handle a response (i.e., "merchant notification") from CyberSource. Arguments: response (dict): Dictionary of parameters received from the payment processor. Keyword Arguments: basket (Basket): Basket being purchased via the payment processor. Raises: AuthorizationError: Authorization was declined. UserCancelled: Indicates the user cancelled payment. TransactionDeclined: Indicates the payment was declined by the processor. GatewayError: Indicates a general error on the part of the processor. InvalidCyberSourceDecision: Indicates an unknown decision value. Known values are ACCEPT, CANCEL, DECLINE, ERROR, REVIEW. PartialAuthorizationError: Indicates only a portion of the requested amount was authorized. Returns: HandledProcessorResponse """ # Validate the signature if not self.is_signature_valid(response): raise InvalidSignatureError # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling # and logging the exception. decision = response['decision'].lower() if decision != 'accept': reason_code = int(response['reason_code']) if decision == 'error' and reason_code == 104: # This means user submitted payment request twice within 15 min. # We need to check if user first payment notification was handled successfuly and user has an order # if user has an order we can raise DuplicateReferenceNumber exception else we need to continue # the order creation process. to upgrade user in correct course mode. if Order.objects.filter( number=response['req_reference_number']).exists(): raise DuplicateReferenceNumber else: logger.info( 'Received duplicate CyberSource payment notification for basket [%d] which is not associated ' 'with any existing order. Continuing to validation and order creation processes.', basket.id, ) else: raise { 'cancel': UserCancelled, 'decline': TransactionDeclined, 'error': GatewayError, 'review': AuthorizationError, }.get(decision, InvalidCybersourceDecision) # Raise an exception if the authorized amount differs from the requested amount. # Note (CCB): We should never reach this point in production since partial authorization is disabled # for our account, and should remain that way until we have a proper solution to allowing users to # complete authorization for the entire order if response['auth_amount'] and response['auth_amount'] != response[ 'req_amount']: raise PartialAuthorizationError currency = response['req_currency'] total = Decimal(response['req_amount']) transaction_id = response.get( 'transaction_id', None) # Error Notifications does not include a transaction id. card_number = response['req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get(response['req_card_type']) return HandledProcessorResponse(transaction_id=transaction_id, total=total, currency=currency, card_number=card_number, card_type=card_type)
def handle_processor_response(self, response, basket=None): """ Handle a response (i.e., "merchant notification") from CyberSource. This method does the following: 1. Verify the validity of the response. 2. Create PaymentEvents and Sources for successful payments. Arguments: response (dict): Dictionary of parameters received from the payment processor. Keyword Arguments: basket (Basket): Basket being purchased via the payment processor. Raises: UserCancelled: Indicates the user cancelled payment. TransactionDeclined: Indicates the payment was declined by the processor. GatewayError: Indicates a general error on the part of the processor. InvalidCyberSourceDecision: Indicates an unknown decision value. Known values are ACCEPT, CANCEL, DECLINE, ERROR. PartialAuthorizationError: Indicates only a portion of the requested amount was authorized. """ # Validate the signature if not self.is_signature_valid(response): raise InvalidSignatureError # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling # and logging the exception. decision = response['decision'].lower() if decision != 'accept': exception = { 'cancel': UserCancelled, 'decline': TransactionDeclined, 'error': GatewayError }.get(decision, InvalidCybersourceDecision) raise exception # Raise an exception if the authorized amount differs from the requested amount. # Note (CCB): We should never reach this point in production since partial authorization is disabled # for our account, and should remain that way until we have a proper solution to allowing users to # complete authorization for the entire order. if response['auth_amount'] != response['req_amount']: raise PartialAuthorizationError # Create Source to track all transactions related to this processor and order source_type, __ = SourceType.objects.get_or_create(name=self.NAME) currency = response['req_currency'] total = Decimal(response['req_amount']) transaction_id = response['transaction_id'] req_card_number = response['req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get(response['req_card_type']) source = Source(source_type=source_type, currency=currency, amount_allocated=total, amount_debited=total, reference=transaction_id, label=req_card_number, card_type=card_type) # Create PaymentEvent to track event_type, __ = PaymentEventType.objects.get_or_create(name=PaymentEventTypeName.PAID) event = PaymentEvent(event_type=event_type, amount=total, reference=transaction_id, processor_name=self.NAME) return source, event
def fulfill_basket(self, basket_id, site): logger.info('Trying to complete order for frozen basket %d', basket_id) basket = self.get_valid_basket(basket_id) if not basket: return False # We need to check if an order for basket exists. try: order = Order.objects.get(basket=basket) logger.info('Basket %d does have a existing order %s', basket.id, basket.order_number) except Order.DoesNotExist: order = None # if no order exists we need to create a new order. if not order: basket.strategy = strategy.Default() # Need to handle the case that applied voucher has been expired. # This will create the order with out discount but subsequently # run the fulfillment to update course mode. try: Applicator().apply(basket, user=basket.owner) except ValueError: basket.clear_vouchers() payment_notification = self.get_payment_notification(basket) if not payment_notification: return False if payment_notification.transaction_id.startswith('PAY'): card_number = 'Paypal Account' card_type = None else: card_number = payment_notification.response['req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get( payment_notification.response['req_card_type']) self.payment_processor = _get_payment_processor( site, payment_notification.processor_name) # Create handled response handled_response = HandledProcessorResponse( transaction_id=payment_notification.transaction_id, total=basket.total_excl_tax, currency=basket.currency, card_number=card_number, card_type=card_type) # Record Payment and try to place order try: self.record_payment( basket=basket, handled_processor_response=handled_response) shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate( basket, shipping_charge) user = basket.owner # Given a basket, order number generation is idempotent. Although we've already # generated this order number once before, it's faster to generate it again # than to retrieve an invoice number from PayPal. order_number = basket.order_number self.handle_order_placement( order_number=order_number, user=user, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=None, order_total=order_total, ) logger.info('Successfully created order for basket %d', basket.id) return True except: # pylint: disable=bare-except logger.exception('Unable to create order for basket %d', basket.id) return False # Start order fulfillment if order exists but is not fulfilled. elif order.is_fulfillable: order_lines = order.lines.all() line_quantities = [line.quantity for line in order_lines] shipping_event, __ = ShippingEventType.objects.get_or_create( name=SHIPPING_EVENT_NAME) EventHandler().handle_shipping_event(order, shipping_event, order_lines, line_quantities) if order.is_fulfillable: logger.error('Unable to fulfill order for basket %d', basket.id) return False return True
def handle_processor_response(self, response, basket=None): """ Handle a response (i.e., "merchant notification") from CyberSource. This method does the following: 1. Verify the validity of the response. 2. Create PaymentEvents and Sources for successful payments. Arguments: response (dict): Dictionary of parameters received from the payment processor. Keyword Arguments: basket (Basket): Basket being purchased via the payment processor. Raises: UserCancelled: Indicates the user cancelled payment. TransactionDeclined: Indicates the payment was declined by the processor. GatewayError: Indicates a general error on the part of the processor. InvalidCyberSourceDecision: Indicates an unknown decision value. Known values are ACCEPT, CANCEL, DECLINE, ERROR. PartialAuthorizationError: Indicates only a portion of the requested amount was authorized. """ # Validate the signature if not self.is_signature_valid(response): raise InvalidSignatureError # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling # and logging the exception. decision = response[u'decision'].lower() if decision != u'accept': exception = { u'cancel': UserCancelled, u'decline': TransactionDeclined, u'error': GatewayError }.get(decision, InvalidCybersourceDecision) raise exception # Raise an exception if the authorized amount differs from the requested amount. # Note (CCB): We should never reach this point in production since partial authorization is disabled # for our account, and should remain that way until we have a proper solution to allowing users to # complete authorization for the entire order. if response[u'auth_amount'] != response[u'req_amount']: raise PartialAuthorizationError # Create Source to track all transactions related to this processor and order source_type, __ = SourceType.objects.get_or_create(name=self.NAME) currency = response[u'req_currency'] total = Decimal(response[u'req_amount']) transaction_id = response[u'transaction_id'] req_card_number = response[u'req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get(response[u'req_card_type']) source = Source(source_type=source_type, currency=currency, amount_allocated=total, amount_debited=total, reference=transaction_id, label=req_card_number, card_type=card_type) # Create PaymentEvent to track event_type, __ = PaymentEventType.objects.get_or_create( name=PaymentEventTypeName.PAID) event = PaymentEvent(event_type=event_type, amount=total, reference=transaction_id, processor_name=self.NAME) return source, event
def fulfill_basket(self, basket_id, site): logger.info('Trying to create order for frozen basket %d', basket_id) try: basket = Basket.objects.get(id=basket_id) except Basket.DoesNotExist: logger.info('Basket %d does not exist', basket_id) return False basket.strategy = strategy.Default() Applicator().apply(basket, user=basket.owner) transaction_responses = basket.paymentprocessorresponse_set.filter( transaction_id__isnull=False) unique_transaction_ids = set( [response.transaction_id for response in transaction_responses]) if len(unique_transaction_ids) > 1: logger.info( 'Basket %d has more than one transaction id, not Fulfilling', basket_id) return False response = transaction_responses[0] if response.transaction_id.startswith('PAY'): card_number = 'PayPal Account' card_type = None self.payment_processor = _get_payment_processor(site, 'paypal') else: card_number = response.response['req_card_number'] card_type = CYBERSOURCE_CARD_TYPE_MAP.get( response.response['req_card_type']) self.payment_processor = _get_payment_processor( site, 'cybersource') handled_response = HandledProcessorResponse( transaction_id=response.transaction_id, total=basket.total_excl_tax, currency=basket.currency, card_number=card_number, card_type=card_type) try: self.record_payment(basket=basket, handled_processor_response=handled_response) shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate( basket, shipping_charge) user = basket.owner # Given a basket, order number generation is idempotent. Although we've already # generated this order number once before, it's faster to generate it again # than to retrieve an invoice number from PayPal. order_number = basket.order_number self.handle_order_placement( order_number=order_number, user=user, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=None, order_total=order_total, ) return True except: # pylint: disable=bare-except logger.exception('Unable to fulfill basket %d', basket.id) return False
def normalize_processor_response(self, response) -> UnhandledCybersourceResponse: decision_map = { 'AUTHORIZED': Decision.accept, 'PARTIAL_AUTHORIZED': Decision.decline, 'AUTHORIZED_PENDING_REVIEW': Decision.authorized_pending_review, 'AUTHORIZED_RISK_DECLINED': Decision.decline, 'PENDING_AUTHENTICATION': Decision.decline, 'PENDING_REVIEW': Decision.review, 'DECLINED': Decision.decline, 'INVALID_REQUEST': Decision.error, } response_json = self.serialize_order_completion(response) if isinstance(response, ApiException): decision = decision_map.get(response_json.get('status'), response_json.get('status')) return UnhandledCybersourceResponse( decision=decision, duplicate_payment=(decision == Decision.error and response_json.get('reason') == 'DUPLICATE_REQUEST'), partial_authorization=False, currency=None, total=None, card_number=None, card_type=None, transaction_id=None, order_id=None, raw_json=response_json, ) decision = decision_map.get(response.status, response.status) currency = None total = None amount_details = response.order_information and response.order_information.amount_details if amount_details: currency = amount_details.currency total = (amount_details.total_amount or amount_details.authorized_amount) if total: total = Decimal(total) card = response.payment_information and response.payment_information.tokenized_card card_type = card and CYBERSOURCE_CARD_TYPE_MAP.get(card.type) return UnhandledCybersourceResponse( decision=decision, duplicate_payment=(decision == Decision.error and response.reason == 'DUPLICATE_REQUEST'), partial_authorization=response.status == 'PARTIAL_AUTHORIZED', currency=currency, total=total, card_number=response.decoded_payment_token['data']['number'], card_type=card_type, transaction_id=response.id, order_id=response.client_reference_information.code, raw_json=response_json, )
def fulfill_basket(self, basket_id, site): logger.info('Trying to complete order for frozen basket %d', basket_id) # Validate the basket. try: basket = Basket.objects.get(id=basket_id) except Basket.DoesNotExist: logger.info('Basket %d does not exist', basket_id) return False # Make sure basket is Frozen which means payment was done. if basket.status != basket.FROZEN: return False # We need to check if an order for basket exists. try: order = Order.objects.get(basket=basket) logger.info('Basket %d does have a existing order %s', basket.id, basket.order_number) except Order.DoesNotExist: order = None # if no order exists we need to create a new order. if not order: basket.strategy = strategy.Default() # Need to handle the case that applied voucher has been expired. # This will create the order with out discount but subsequently # run the fulfillment to update course mode. try: Applicator().apply(basket, user=basket.owner) except ValueError: basket.clear_vouchers() # Filter the successful payment processor response which in case # of Cybersource includes "u'decision': u'ACCEPT'" and in case of # Paypal includes "u'state': u'approved'". successful_transaction = basket.paymentprocessorresponse_set.filter( Q(response__contains='ACCEPT') | Q(response__contains='approved')) # In case of no successful transactions log and stop the process. if not successful_transaction: logger.info( 'Basket %d does not have any successful payment response', basket_id) return False unique_transaction_ids = set([ response.transaction_id for response in successful_transaction ]) if len(unique_transaction_ids) > 1: logger.warning( 'Basket %d has more than one successful transaction id, using the first one', basket_id) successful_payment_notification = successful_transaction[0] self.payment_processor = _get_payment_processor( site, successful_payment_notification.processor_name) # Create handled response handled_response = HandledProcessorResponse( transaction_id=successful_payment_notification.transaction_id, total=basket.total_excl_tax, currency=basket.currency, card_number=json.loads( successful_payment_notification.response).get( 'req_card_number', 'Paypal Account'), card_type=CYBERSOURCE_CARD_TYPE_MAP.get( json.loads(successful_payment_notification.response).get( 'req_card_type'), None)) # Record Payment and try to place order try: self.record_payment( basket=basket, handled_processor_response=handled_response) shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate( basket, shipping_charge) user = basket.owner # Given a basket, order number generation is idempotent. Although we've already # generated this order number once before, it's faster to generate it again # than to retrieve an invoice number from PayPal. order_number = basket.order_number self.handle_order_placement( order_number=order_number, user=user, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=None, order_total=order_total, ) logger.info('Successfully created order for basket %d', basket.id) return True except: # pylint: disable=bare-except logger.exception('Unable to create order for basket %d', basket.id) return False # Start order fulfillment if order exists but is not fulfilled. elif order.is_fulfillable: order_lines = order.lines.all() line_quantities = [line.quantity for line in order_lines] shipping_event, __ = ShippingEventType.objects.get_or_create( name=SHIPPING_EVENT_NAME) EventHandler().handle_shipping_event(order, shipping_event, order_lines, line_quantities) if order.is_fulfillable: logger.error('Unable to fulfill order for basket %d', basket.id) return False return True