def clean_max_global_applications(self): # enterprise offers have their own cleanup logic if self.priority != OFFER_PRIORITY_ENTERPRISE and self.max_global_applications is not None: if not isinstance(self.max_global_applications, six.integer_types) or self.max_global_applications < 1: log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. max_global_applications field must be a positive number.' )
def clean_code(self): if not self.code: log_message_and_raise_validation_error('Failed to create Voucher. Voucher code must be set.') if not self.code.isalnum(): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher code must contain only alphanumeric characters.' )
def update_offer_data(self, request_data, vouchers, site): """ Remove all offers from the vouchers and add a new offer Arguments: request_data (dict): the request parameters sent via api. vouchers (list): the vouchers attached to this coupon to update. site (Site): the site for this request. """ benefit_value = request_data.get('benefit_value') enterprise_customer = request_data.get('enterprise_customer', {}).get('id', None) enterprise_catalog = request_data.get( 'enterprise_customer_catalog') or None max_uses = request_data.get('max_uses') email_domains = request_data.get('email_domains') # Validate max_uses if max_uses is not None: if vouchers.first().usage == Voucher.SINGLE_USE: log_message_and_raise_validation_error( 'Failed to update Coupon. ' 'max_global_applications field cannot be set for voucher type [{voucher_type}].' .format(voucher_type=Voucher.SINGLE_USE)) try: max_uses = int(max_uses) if max_uses < 1: raise ValueError except ValueError: raise ValidationError( 'max_global_applications field must be a positive number.') coupon_was_migrated = False for voucher in vouchers.all(): updated_enterprise_offer = update_voucher_with_enterprise_offer( offer=voucher.enterprise_offer, benefit_value=benefit_value, max_uses=max_uses, enterprise_customer=enterprise_customer, enterprise_catalog=enterprise_catalog, email_domains=email_domains, site=site, ) updated_orginal_offer = None if voucher.original_offer != voucher.enterprise_offer: coupon_was_migrated = True updated_orginal_offer = update_voucher_offer( offer=voucher.original_offer, benefit_value=benefit_value, max_uses=max_uses, email_domains=email_domains, site=site, ) voucher.offers.clear() voucher.offers.add(updated_enterprise_offer) if updated_orginal_offer: voucher.offers.add(updated_orginal_offer) if coupon_was_migrated: super(EnterpriseCouponViewSet, self).update_range_data(request_data, vouchers)
def clean_max_global_applications(self): if self.max_global_applications is not None: if self.max_global_applications < 1 or not isinstance( self.max_global_applications, (int, long)): log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. max_global_applications field must be a positive number.' )
def _create_new_voucher(code, coupon, end_datetime, name, offer, start_datetime, voucher_type): """ Creates a voucher. If randomly generated voucher code already exists, new code will be generated and reverified. Args: code (str): Code associated with vouchers. If not provided, one will be generated. coupon (Product): Coupon product associated with voucher. end_datetime (datetime): Voucher end date. name (str): Voucher name. offer (Offer): Offer associated with voucher. start_datetime (datetime): Voucher start date. voucher_type (str): Voucher usage. Returns: Voucher """ if offer.benefit.type == Benefit.PERCENTAGE and offer.benefit.value == 100 and code: log_message_and_raise_validation_error( 'Failed to create Voucher. Code may not be set for enrollment coupon.' ) voucher_code = code or _generate_code_string(settings.VOUCHER_CODE_LENGTH) if not end_datetime: log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher end datetime field must be set.' ) elif not isinstance(end_datetime, datetime.datetime): try: end_datetime = dateutil.parser.parse(end_datetime) except (AttributeError, ValueError): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher end datetime value [{date}] is invalid.' .format(date=end_datetime)) if not start_datetime: log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher start datetime field must be set.' ) elif not isinstance(start_datetime, datetime.datetime): try: start_datetime = dateutil.parser.parse(start_datetime) except (AttributeError, ValueError): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher start datetime [{date}] is invalid.' .format(date=start_datetime)) voucher = Voucher.objects.create(name=name, code=voucher_code, usage=voucher_type, start_datetime=start_datetime, end_datetime=end_datetime) voucher.offers.add(offer) coupon_voucher, __ = CouponVouchers.objects.get_or_create(coupon=coupon) coupon_voucher.vouchers.add(voucher) return voucher
def clean_percentage(self): if not self.range: log_message_and_raise_validation_error( 'Percentage benefits require a product range') if self.value > 100: log_message_and_raise_validation_error( 'Percentage discount cannot be greater than 100')
def save(self, *args, **kwargs): try: if not isinstance(self.attr.note, basestring) and self.attr.note is not None: log_message_and_raise_validation_error( 'Failed to create Product. Product note value must be of type string' ) except AttributeError: pass super(Product, self).save(*args, **kwargs) # pylint: disable=bad-super-call
def clean_datetimes(self): if not (self.end_datetime and self.start_datetime): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher start and end datetime fields must be set.' ) if not (isinstance(self.end_datetime, datetime.datetime) and isinstance(self.start_datetime, datetime.datetime)): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher start and end datetime fields must be type datetime.' )
def remind(self, request, pk): # pylint: disable=unused-argument """ Remind users of pending offer assignments by email. """ coupon = self.get_object() email_template = request.data.pop('template', None) if not email_template: log_message_and_raise_validation_error( str('Template is required.')) if request.data.get('assignments'): assignments = request.data.get('assignments') else: # If no assignment is passed, send reminder to all assignments associated with the coupon. vouchers = coupon.attr.coupon_vouchers.vouchers.all() code_filter = request.data.get('code_filter') if not code_filter: raise serializers.ValidationError( 'code_filter must be specified') if code_filter == VOUCHER_NOT_REDEEMED: assignment_usages = self._get_not_redeemed_usages(vouchers) elif code_filter == VOUCHER_PARTIAL_REDEEMED: assignment_usages = self._get_partial_redeemed_usages(vouchers) else: raise serializers.ValidationError( 'Invalid code_filter specified: {}'.format(code_filter)) assignments = [{ 'code': assignment['code'], 'email': assignment['user_email'] } for assignment in assignment_usages] serializer = CouponCodeRemindSerializer(data=assignments, many=True, context={ 'coupon': coupon, 'template': email_template }) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def clean_email_domains(self): if self.email_domains: if not isinstance(self.email_domains, str): log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. ConditionalOffer email domains must be of type string.' ) email_domains_array = self.email_domains.split(',') if not email_domains_array[-1]: log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. ' 'Trailing comma for ConditionalOffer email domains is not allowed.' ) for domain in email_domains_array: domain_parts = domain.split('.') error_message = 'Failed to create ConditionalOffer. ' \ 'Email domain [{email_domain}] is invalid.'.format(email_domain=domain) # Conditions being tested: # - double hyphen not allowed # - must contain at least one dot # - top level domain must be at least two characters long # - hyphens are not allowed in top level domain # - numbers are not allowed in top level domain if any([ '--' in domain, len(domain_parts) < 2, len(domain_parts[-1]) < 2, re.findall(r'[-0-9]', domain_parts[-1]) ]): log_message_and_raise_validation_error(error_message) for domain_part in domain_parts: # - non of the domain levels can start or end with a hyphen before encoding if domain_part.startswith('-') or domain_part.endswith( '-'): log_message_and_raise_validation_error(error_message) # - all encoded domain levels must match given regex expression if not re.match(r'^([a-z0-9-]+)$', domain_part.encode('idna').decode()): log_message_and_raise_validation_error(error_message)
def update_offer_data(self, data, vouchers, coupon_id): offer_data = self.create_update_data_dict(data=data, fields=ConditionalOffer.UPDATABLE_OFFER_FIELDS) if offer_data: if offer_data.get('max_global_applications') is not None: if vouchers.first().usage == Voucher.SINGLE_USE: log_message_and_raise_validation_error( 'Failed to update Coupon [{coupon_id}]. ' 'max_global_applications field cannot be set for voucher type [{voucher_type}].'.format( coupon_id=coupon_id, voucher_type=Voucher.SINGLE_USE ) ) try: offer_data['max_global_applications'] = int(offer_data['max_global_applications']) if offer_data['max_global_applications'] < 1: raise ValueError except ValueError: raise ValidationError('max_global_applications field must be a positive number.') ConditionalOffer.objects.filter(vouchers__in=vouchers.all()).update(**offer_data)
def save(self, *args, **kwargs): try: if not isinstance(self.attr.note, str) and self.attr.note is not None: log_message_and_raise_validation_error( 'Failed to create Product. Product note value must be of type string' ) except AttributeError: pass try: if self.attr.notify_email is not None: validate_email(self.attr.notify_email) except ValidationError: log_message_and_raise_validation_error( 'Notification email must be a valid email address.') except AttributeError: pass super(Product, self).save(*args, **kwargs) # pylint: disable=bad-super-call
def remind(self, request, pk): # pylint: disable=unused-argument """ Remind users of pending offer assignments by email. """ coupon = self.get_object() email_template = request.data.pop('template', None) if not email_template: log_message_and_raise_validation_error( str('Template is required.')) serializer = CouponCodeRemindSerializer( data=request.data.get('assignments'), many=True, context={ 'coupon': coupon, 'template': email_template }) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def update_offer_data(self, data, vouchers, coupon_id): offer_data = self.create_update_data_dict(data=data, fields=ConditionalOffer.UPDATABLE_OFFER_FIELDS) if offer_data: if offer_data.get('max_global_applications') is not None: if vouchers.first().usage == Voucher.SINGLE_USE: log_message_and_raise_validation_error( 'Failed to update Coupon [{coupon_id}]. ' 'max_global_applications field cannot be set for voucher type [{voucher_type}].'.format( coupon_id=coupon_id, voucher_type=Voucher.SINGLE_USE ) ) try: offer_data['max_global_applications'] = int(offer_data['max_global_applications']) if offer_data['max_global_applications'] < 1: raise ValueError except ValueError: log_message_and_raise_validation_error( 'Failed to update Coupon [{coupon_id}]. ' 'max_global_applications field must be a positive number.'.format(coupon_id=coupon_id) ) ConditionalOffer.objects.filter(vouchers__in=vouchers.all()).update(**offer_data)
def clean(self): """ Validation for model fields. """ if self.catalog and (self.course_catalog or self.catalog_query or self.course_seat_types): log_message_and_raise_validation_error( 'Failed to create Range. Catalog and dynamic catalog fields may not be set in the same range.' ) error_message = 'Failed to create Range. Either catalog_query or course_catalog must be given but not both ' \ 'and course_seat_types fields must be set.' if self.catalog_query and self.course_catalog: log_message_and_raise_validation_error(error_message) elif (self.catalog_query or self.course_catalog) and not self.course_seat_types: log_message_and_raise_validation_error(error_message) elif self.course_seat_types and not (self.catalog_query or self.course_catalog): log_message_and_raise_validation_error(error_message) if self.course_seat_types: validate_credit_seat_type(self.course_seat_types)
def clean(self): """ Validation for model fields. """ if self.catalog and (self.catalog_query or self.course_seat_types): log_message_and_raise_validation_error( 'Failed to create Range. Catalog and dynamic catalog fields may not be set in the same range.' ) # Both catalog_query and course_seat_types must be set or empty error_message = 'Failed to create Range. Both catalog_query and course_seat_types fields must be set.' if self.catalog_query and not self.course_seat_types: log_message_and_raise_validation_error(error_message) elif self.course_seat_types and not self.catalog_query: log_message_and_raise_validation_error(error_message) if self.course_seat_types: validate_credit_seat_type(self.course_seat_types)
def validate_credit_seat_type(course_seat_types): if not isinstance(course_seat_types, basestring): log_message_and_raise_validation_error('Failed to create Range. Credit seat types must be of type string.') course_seat_types_list = course_seat_types.split(',') if len(course_seat_types_list) > 1 and 'credit' in course_seat_types_list: log_message_and_raise_validation_error( 'Failed to create Range. Credit seat type cannot be paired with other seat types.' ) if not set(course_seat_types_list).issubset(set(Range.ALLOWED_SEAT_TYPES)): log_message_and_raise_validation_error( 'Failed to create Range. Not allowed course seat types {}. ' 'Allowed values for course seat types are {}.'.format(course_seat_types_list, Range.ALLOWED_SEAT_TYPES) )
def create_vouchers( benefit_type, benefit_value, catalog, coupon, end_datetime, enterprise_customer, name, quantity, start_datetime, voucher_type, code=None, max_uses=None, _range=None, catalog_query=None, course_seat_types=None, email_domains=None, course_catalog=None, program_uuid=None, site=None, ): """ Create vouchers. Arguments: benefit_type (str): Type of benefit associated with vouchers. benefit_value (Decimal): Value of benefit associated with vouchers. catalog (Catalog): Catalog associated with range of products to which a voucher can be applied to. catalog_query (str): ElasticSearch query used by dynamic coupons. Defaults to None. code (str): Code associated with vouchers. Defaults to None. coupon (Coupon): Coupon entity associated with vouchers. course_catalog (int): Course catalog id from Discovery Service. Defaults to None. course_seat_types (str): Comma-separated list of course seat types. email_domains (str): List of email domains to restrict coupons. Defaults to None. end_datetime (datetime): End date for voucher offer. enterprise_customer (str): UUID of an EnterpriseCustomer attached to this voucher max_uses (int): Number of Voucher max uses. Defaults to None. name (str): Voucher name. quantity (int): Number of vouchers to be created. start_datetime (datetime): Start date for voucher offer. voucher_type (str): Type of voucher. _range (Range): Product range. Defaults to None. program_uuid (str): Program UUID. Defaults to None. site (site): Site for which the Coupon is created. Defaults to None. Returns: List[Voucher] """ logger.info("Creating [%d] vouchers product [%s]", quantity, coupon.id) vouchers = [] offers = [] # Maximum number of uses can be set for each voucher type and disturb # the predefined behaviours of the different voucher types. Therefor # here we enforce that the max_uses variable can't be used for SINGLE_USE # voucher types. if max_uses is not None: if voucher_type == Voucher.SINGLE_USE: log_message_and_raise_validation_error( 'Failed to create Voucher. max_uses field cannot be set for voucher type [{voucher_type}].'.format( voucher_type=Voucher.SINGLE_USE ) ) try: max_uses = int(max_uses) except ValueError: raise log_message_and_raise_validation_error('Failed to create Voucher. max_uses field must be a number.') if _range: # Enrollment codes use a custom range. logger.info("Creating [%d] enrollment code vouchers", quantity) product_range = _range else: logger.info("Creating [%d] vouchers for coupon [%s]", quantity, coupon.id) range_name = (_('Range for coupon [{coupon_id}]').format(coupon_id=coupon.id)) # make sure course catalog is None if its empty course_catalog = course_catalog if course_catalog else None # make sure enterprise_customer is None if it's empty enterprise_customer = enterprise_customer or None # we do not need a range if this is for a Program if program_uuid: product_range = None else: product_range, __ = Range.objects.get_or_create( name=range_name, catalog=catalog, catalog_query=catalog_query, course_catalog=course_catalog, course_seat_types=course_seat_types, enterprise_customer=enterprise_customer, ) # In case of more than 1 multi-usage coupon, each voucher needs to have an individual # offer because the usage is tied to the offer so that a usage on one voucher would # mean all vouchers will have their usage decreased by one, hence each voucher needs # its own offer to keep track of its own usages without interfering with others. multi_offer = True if ( voucher_type == Voucher.MULTI_USE or voucher_type == Voucher.ONCE_PER_CUSTOMER ) else False num_of_offers = quantity if multi_offer else 1 for num in range(num_of_offers): offer = _get_or_create_offer( product_range=product_range, benefit_type=benefit_type, benefit_value=benefit_value, max_uses=max_uses, coupon_id=coupon.id, offer_number=num, email_domains=email_domains, program_uuid=program_uuid, site=site ) offers.append(offer) for i in range(quantity): voucher = _create_new_voucher( end_datetime=end_datetime, offer=offers[i] if multi_offer else offers[0], start_datetime=start_datetime, voucher_type=voucher_type, code=code, name=name ) vouchers.append(voucher) return vouchers
def clean_value(self): if self.value < 0: log_message_and_raise_validation_error( 'Failed to create Benefit. Benefit value may not be a negative number.' )
def save(self, *args, **kwargs): # pylint: disable=arguments-differ if not self.name: log_message_and_raise_validation_error( 'Failed to create BusinessClient. BusinessClient name may not be empty.' ) super(BusinessClient, self).save(*args, **kwargs)
def update_offer_data(self, request_data, vouchers, site): """ Remove all offers from the vouchers and add a new offer Arguments: coupon (Product): Coupon product associated with vouchers vouchers (ManyRelatedManager): Vouchers associated with the coupon to be updated site (Site): The Site associated with this offer benefit_value (Decimal): Benefit value associated with a new offer program_uuid (str): Program UUID enterprise_customer (str): Enterprise Customer UUID enterprise_catalog (str): Enterprise Catalog UUID """ program_uuid = request_data.get('program_uuid') benefit_value = request_data.get('benefit_value') enterprise_customer_data = request_data.get('enterprise_customer') enterprise_customer = enterprise_customer_data.get('id', None) if enterprise_customer_data else None enterprise_catalog = request_data.get('enterprise_customer_catalog') or None max_uses = request_data.get('max_uses') email_domains = request_data.get('email_domains') # Validate max_uses if max_uses is not None: if vouchers.first().usage == Voucher.SINGLE_USE: log_message_and_raise_validation_error( 'Failed to update Coupon. ' 'max_global_applications field cannot be set for voucher type [{voucher_type}].'.format( voucher_type=Voucher.SINGLE_USE )) try: max_uses = int(max_uses) if max_uses < 1: raise ValueError except ValueError: raise ValidationError('max_global_applications field must be a positive number.') for voucher in vouchers.all(): updated_original_offer = update_voucher_offer( offer=voucher.original_offer, benefit_value=benefit_value, max_uses=max_uses, program_uuid=program_uuid, email_domains=email_domains, site=site, ) updated_enterprise_offer = None if voucher.enterprise_offer: updated_enterprise_offer = update_voucher_with_enterprise_offer( offer=voucher.enterprise_offer, benefit_value=benefit_value, max_uses=max_uses, enterprise_customer=enterprise_customer, enterprise_catalog=enterprise_catalog, email_domains=email_domains, site=site, ) elif enterprise_customer: # If we are performing an update on an existing enterprise coupon, # we need to ensure the enterprise offer is created if it didn't already exist. updated_enterprise_offer = get_or_create_enterprise_offer( benefit_value=benefit_value or voucher.original_offer.benefit.value, benefit_type=voucher.original_offer.benefit.type, enterprise_customer=enterprise_customer, enterprise_customer_catalog=enterprise_catalog, offer_name=voucher.original_offer.name + " ENT Offer", max_uses=max_uses or voucher.original_offer.max_global_applications, email_domains=email_domains or voucher.original_offer.email_domains, site=site or voucher.original_offer.site, ) voucher.offers.clear() voucher.offers.add(updated_original_offer) if updated_enterprise_offer: voucher.offers.add(updated_enterprise_offer)
def _get_or_create_offer(product_range, benefit_type, benefit_value, offer_name, max_uses, site, email_domains=None, program_uuid=None): """ Return an offer for a catalog with condition and benefit. If offer doesn't exist, new offer will be created and associated with provided Offer condition and benefit. Args: product_range (Range): Range of products associated with condition benefit_type (str): Type of benefit associated with the offer benefit_value (Decimal): Value of benefit associated with the offer Kwargs: coupon_id (int): ID of the coupon max_uses (int): number of maximum global application number an offer can have offer_number (int): number of the consecutive offer - used in case of a multiple multi-use coupon email_domains (str): a comma-separated string of email domains allowed to apply this offer program_uuid (str): the Program UUID site (site): Site for which the Coupon is created. Defaults to None. Returns: Offer """ if program_uuid: try: offer_condition = ProgramCourseRunSeatsCondition.objects.get( program_uuid=program_uuid) except ProgramCourseRunSeatsCondition.DoesNotExist: offer_condition = create_condition(ProgramCourseRunSeatsCondition, program_uuid=program_uuid) else: offer_condition, __ = Condition.objects.get_or_create( proxy_class=None, range=product_range, type=Condition.COUNT, value=1, ) try: if program_uuid: proxy_class = class_path(BENEFIT_MAP[benefit_type]) offer_benefit = Benefit.objects.filter( proxy_class=proxy_class, value=benefit_value).first() if not offer_benefit: offer_benefit = Benefit() offer_benefit.proxy_class = proxy_class offer_benefit.value = benefit_value offer_benefit.save() offer_name = "{}-{}".format(offer_name, offer_benefit.name) else: offer_benefit, __ = Benefit.objects.get_or_create( range=product_range, type=benefit_type, value=Decimal(benefit_value), max_affected_items=1, ) except ( TypeError, DecimalException ): # If the benefit_value parameter is not sent TypeError will be raised log_message_and_raise_validation_error( 'Failed to create Benefit. Benefit value must be a positive number or 0.' ) offer_kwargs = { 'offer_type': ConditionalOffer.VOUCHER, 'condition': offer_condition, 'benefit': offer_benefit, 'max_global_applications': int(max_uses) if max_uses is not None else None, 'email_domains': email_domains, 'site': site, 'partner': site.siteconfiguration.partner if site else None, 'priority': OFFER_PRIORITY_VOUCHER, } offer, __ = ConditionalOffer.objects.update_or_create( name=offer_name, defaults=offer_kwargs) return offer
def validate_voucher_fields(max_uses, voucher_type, benefit_type, benefit_value, code, end_datetime, start_datetime): # Maximum number of uses can be set for each voucher type and disturb # the predefined behaviours of the different voucher types. Therefor # here we enforce that the max_uses variable can't be used for SINGLE_USE # voucher types. if max_uses is not None: if voucher_type == Voucher.SINGLE_USE: log_message_and_raise_validation_error( 'Failed to create Voucher. max_uses field cannot be set for voucher type [{voucher_type}].' .format(voucher_type=Voucher.SINGLE_USE)) try: int(max_uses) except ValueError: raise log_message_and_raise_validation_error( 'Failed to create Voucher. max_uses field must be a number.') if benefit_type == Benefit.PERCENTAGE and benefit_value == 100 and code: log_message_and_raise_validation_error( 'Failed to create Voucher. Code may not be set for enrollment coupon.' ) if not end_datetime: log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher end datetime field must be set.' ) elif not isinstance(end_datetime, datetime.datetime): try: dateutil.parser.parse(end_datetime) except (AttributeError, ValueError, TypeError): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher end datetime value [{date}] is invalid.' .format(date=end_datetime)) if not start_datetime: log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher start datetime field must be set.' ) elif not isinstance(start_datetime, datetime.datetime): try: dateutil.parser.parse(start_datetime) except (AttributeError, ValueError, TypeError): log_message_and_raise_validation_error( 'Failed to create Voucher. Voucher start datetime [{date}] is invalid.' .format(date=start_datetime))
def clean_type(self): if self.type not in self.VALID_BENEFIT_TYPES: log_message_and_raise_validation_error( 'Failed to create Benefit. Unrecognised benefit type [{type}]'. format(type=self.type))
def clean_type(self): if self.type not in self.VALID_BENEFIT_TYPES: log_message_and_raise_validation_error( 'Failed to create Benefit. Unrecognised benefit type [{type}]'.format(type=self.type) )
def save(self, *args, **kwargs): if not self.name: log_message_and_raise_validation_error( 'Failed to create BusinessClient. BusinessClient name may not be empty.' ) super(BusinessClient, self).save(*args, **kwargs)
def _get_or_create_offer(product_range, benefit_type, benefit_value, coupon_id=None, max_uses=None, offer_number=None, email_domains=None): """ Return an offer for a catalog with condition and benefit. If offer doesn't exist, new offer will be created and associated with provided Offer condition and benefit. Args: product_range (Range): Range of products associated with condition benefit_type (str): Type of benefit associated with the offer benefit_value (Decimal): Value of benefit associated with the offer Kwargs: coupon_id (int): ID of the coupon max_uses (int): number of maximum global application number an offer can have offer_number (int): number of the consecutive offer - used in case of a multiple multi-use coupon email_domains (str): a comma-separated string of email domains allowed to apply this offer Returns: Offer """ offer_condition, __ = Condition.objects.get_or_create( range=product_range, type=Condition.COUNT, value=1, ) try: offer_benefit, __ = Benefit.objects.get_or_create( range=product_range, type=benefit_type, value=Decimal(benefit_value), max_affected_items=1, ) except ( TypeError, DecimalException ): # If the benefit_value parameter is not sent TypeError will be raised log_message_and_raise_validation_error( 'Failed to create Benefit. Benefit value must be a positive number or 0.' ) offer_name = "Coupon [{}]-{}-{}".format(coupon_id, offer_benefit.type, offer_benefit.value) if offer_number: offer_name = "{} [{}]".format(offer_name, offer_number) offer, __ = ConditionalOffer.objects.get_or_create( name=offer_name, offer_type=ConditionalOffer.VOUCHER, condition=offer_condition, benefit=offer_benefit, max_global_applications=max_uses, email_domains=email_domains) return offer
def clean_email_domains(self): if self.email_domains == '': log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. ConditionalOffer email domains may not be an empty string.' ) if self.email_domains: if not isinstance(self.email_domains, basestring): log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. ConditionalOffer email domains must be of type string.' ) email_domains_array = self.email_domains.split(',') if not email_domains_array[-1]: log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. ' 'Trailing comma for ConditionalOffer email domains is not allowed.' ) for domain in email_domains_array: domain_parts = domain.split('.') error_message = 'Failed to create ConditionalOffer. ' \ 'Email domain [{email_domain}] is invalid.'.format(email_domain=domain) # Conditions being tested: # - double hyphen not allowed # - must contain at least one dot # - top level domain must be at least two characters long # - hyphens are not allowed in top level domain # - numbers are not allowed in top level domain if any(['--' in domain, len(domain_parts) < 2, len(domain_parts[-1]) < 2, re.findall(r'[-0-9]', domain_parts[-1])]): log_message_and_raise_validation_error(error_message) for domain_part in domain_parts: # - non of the domain levels can start or end with a hyphen before encoding if domain_part.startswith('-') or domain_part.endswith('-'): log_message_and_raise_validation_error(error_message) # - all encoded domain levels must match given regex expression if not re.match(r'^([a-z0-9-]+)$', domain_part.encode('idna')): log_message_and_raise_validation_error(error_message)
def clean_max_global_applications(self): if self.max_global_applications is not None: if self.max_global_applications < 1 or not isinstance(self.max_global_applications, (int, long)): log_message_and_raise_validation_error( 'Failed to create ConditionalOffer. max_global_applications field must be a positive number.' )