Ejemplo n.º 1
0
def generate_client_prices_for_stylist_services(
        stylist: Stylist,
        services: List[StylistService],
        client: Optional[Client],
        exclude_fully_booked: bool = False,
        exclude_unavailable_days: bool = False) -> List[ClientPriceOnDate]:

    prices_and_dates: Iterable[
        PriceOnDate] = generate_prices_for_stylist_service(
            services, client, exclude_fully_booked, exclude_unavailable_days)
    client_prices_on_dates: List[ClientPriceOnDate] = []

    for obj in prices_and_dates:
        availability_on_day = stylist.available_days.filter(
            weekday=obj.date.isoweekday(), is_available=True).last(
            ) if obj.date == stylist.get_current_now().date() else None
        stylist_eod = stylist.salon.timezone.localize(
            datetime.datetime.combine(date=obj.date,
                                      time=availability_on_day.work_end_at)
        ) if availability_on_day else None
        if not stylist_eod or stylist.get_current_now() < (
                stylist_eod - stylist.service_time_gap -
                datetime.timedelta(minutes=END_OF_DAY_BUFFER_TIME_IN_MINUTES)):
            client_prices_on_dates.append(
                ClientPriceOnDate(
                    date=obj.date,
                    price=int(
                        Decimal(obj.calculated_price.price).quantize(
                            0, ROUND_HALF_UP)),
                    is_fully_booked=obj.is_fully_booked,
                    is_working_day=obj.is_working_day,
                    discount_type=obj.calculated_price.applied_discount,
                ))
    return client_prices_on_dates
Ejemplo n.º 2
0
 def test_get_date_range_discount_percent(self, stylist_data: Stylist):
     assert (stylist_data.get_date_range_discount_percent(
         datetime.date(2018, 4, 9)) == 30)
     assert (stylist_data.get_date_range_discount_percent(
         datetime.date(2018, 4, 10)) == 30)
     assert (stylist_data.get_date_range_discount_percent(
         datetime.date(2018, 4, 12)) == 0)
Ejemplo n.º 3
0
def has_bookable_slots_with_discounts(stylist: Stylist,
                                      max_dates_to_look: int = 7):
    """
    Return True if stylist is bookable and has at least one available slot
    with non-zero discounts in the next `max_dates_to_look` days (excluding today)
    :param stylist: Stylist to check
    :param max_dates_to_look: for how many days in the future to look
    :return: True if has bookable discounted slots, False otherwise
    """

    today = stylist.with_salon_tz(timezone.now()).date()
    has_available_slots_with_discounts = False
    day_count = 0
    # go over next 7 days, and find first available slot on a day for which stylist
    # has non-zero discount
    while not has_available_slots_with_discounts and day_count <= max_dates_to_look:
        day_count += 1
        date_to_verify = today + datetime.timedelta(days=day_count)
        available_slots = list(
            filter(lambda a: not a.is_booked,
                   stylist.get_available_slots(date=date_to_verify)))
        available_slot_count = len(available_slots)
        if available_slot_count:
            has_discount_on_this_day = stylist.get_weekday_discount_percent(
                Weekday(date_to_verify.isoweekday())) > 0
            if has_discount_on_this_day:
                has_available_slots_with_discounts = True
    return has_available_slots_with_discounts
Ejemplo n.º 4
0
def create_stripe_account_for_stylist(stylist: Stylist, auth_code: str):
    """Create connected account for stylist in stripe, and save data to DB"""
    (account_id, stripe_access_token, stripe_refresh_token
     ) = create_connected_account_from_client_token(auth_code)
    stylist.stripe_account_id = account_id
    stylist.stripe_access_token = stripe_access_token
    stylist.stripe_refresh_token = stripe_refresh_token
    stylist.save(update_fields=[
        'stripe_account_id', 'stripe_access_token', 'stripe_refresh_token'
    ])
Ejemplo n.º 5
0
def get_next_deal_of_week_date(stylist: Stylist) -> Optional[datetime.date]:
    """Return date of next deal of the week if set by stylist, else None"""
    deal_of_week_weekday: Optional[int] = stylist.get_deal_of_week_weekday()
    if not deal_of_week_weekday:
        return None
    today = stylist.with_salon_tz(timezone.now()).date()
    today_weekday = today.isoweekday()
    if today_weekday < deal_of_week_weekday:
        return today + datetime.timedelta(deal_of_week_weekday - today_weekday)
    return today + datetime.timedelta(deal_of_week_weekday - today_weekday + 7)
Ejemplo n.º 6
0
 def get_weekdays(self, stylist: Stylist):
     weekday_availability = [
         stylist.get_or_create_weekday_availability(Weekday(weekday))
         for weekday in range(1, 8)
     ]
     return StylistAvailableWeekDaySerializer(weekday_availability,
                                              many=True).data
Ejemplo n.º 7
0
    def test_get_today_appointments(
        self,
        stylist_data: Stylist,
    ):
        appointments: Dict[str, Appointment] = stylist_appointments_data(
            stylist_data)
        today_appointments = [
            a.id for a in stylist_data.get_today_appointments(
                upcoming_only=True,
                exclude_statuses=[
                    AppointmentStatus.CANCELLED_BY_CLIENT,
                    AppointmentStatus.CANCELLED_BY_STYLIST,
                ])
        ]

        assert (len(today_appointments) == 3)
        assert (appointments['current_appointment'].id in today_appointments)
        assert (appointments['future_appointment'].id in today_appointments)
        assert (appointments['late_night_appointment'].id
                in today_appointments)
        assert (appointments['next_day_appointment'].id
                not in today_appointments)
        assert (appointments['past_appointment'].id not in today_appointments)

        today_appointments = [
            a.id for a in stylist_data.get_today_appointments(
                upcoming_only=False,
                exclude_statuses=[
                    AppointmentStatus.CANCELLED_BY_CLIENT,
                    AppointmentStatus.CANCELLED_BY_STYLIST
                ])
        ]

        assert (len(today_appointments) == 4)
        assert (appointments['current_appointment'].id in today_appointments)
        assert (appointments['past_appointment'].id in today_appointments)
        assert (appointments['future_appointment'].id in today_appointments)
        assert (appointments['late_night_appointment'].id
                in today_appointments)
        assert (appointments['next_day_appointment'].id
                not in today_appointments)
Ejemplo n.º 8
0
    def test_set_status(self, stylist_data: Stylist):
        appointment: Appointment = G(Appointment,
                                     stylist=stylist_data,
                                     duration=datetime.timedelta())
        assert (appointment.status == AppointmentStatus.NEW)

        appointment.set_status(AppointmentStatus.CANCELLED_BY_CLIENT,
                               stylist_data.user)
        appointment.refresh_from_db()
        assert (appointment.status == AppointmentStatus.CANCELLED_BY_CLIENT)
        assert (appointment.status_history.latest('updated_at').updated_by ==
                stylist_data.user)
        assert (appointment.status_history.latest('updated_at').updated_at ==
                stylist_data.get_current_now())
Ejemplo n.º 9
0
    def test_get_appointments_in_datetime_range(
        self,
        stylist_data: Stylist,
    ):
        appointments: Dict[str, Appointment] = stylist_appointments_data(
            stylist_data)
        all_appointments = stylist_data.get_appointments_in_datetime_range()

        assert (all_appointments.count() == 7)
        appointments_from_start = stylist_data.get_appointments_in_datetime_range(
            datetime_from=None,
            datetime_to=stylist_data.get_current_now(),
            including_to=True,
            exclude_statuses=[
                AppointmentStatus.CANCELLED_BY_CLIENT,
                AppointmentStatus.CANCELLED_BY_STYLIST
            ])
        assert (frozenset([a.id
                           for a in appointments_from_start]) == frozenset([
                               appointments['past_appointment'].id,
                               appointments['current_appointment'].id,
                               appointments['last_week_appointment'].id,
                           ]))

        apppointmens_to_end = stylist_data.get_appointments_in_datetime_range(
            datetime_from=stylist_data.get_current_now(),
            datetime_to=None,
            exclude_statuses=[
                AppointmentStatus.CANCELLED_BY_CLIENT,
                AppointmentStatus.CANCELLED_BY_STYLIST
            ])
        assert (frozenset([a.id for a in apppointmens_to_end]) == frozenset([
            appointments['current_appointment'].id,
            appointments['future_appointment'].id,
            appointments['late_night_appointment'].id,
            appointments['next_day_appointment'].id,
            appointments['next_week_appointment'].id,
        ]))

        appointments_between = stylist_data.get_appointments_in_datetime_range(
            datetime_from=pytz.timezone('UTC').localize(
                datetime.datetime(2018, 5, 13)),
            datetime_to=pytz.timezone('UTC').localize(
                datetime.datetime(2018, 5, 15, 23, 59, 59)),
            exclude_statuses=[
                AppointmentStatus.CANCELLED_BY_CLIENT,
                AppointmentStatus.CANCELLED_BY_STYLIST
            ])
        assert (frozenset([a.id for a in appointments_between]) == frozenset([
            appointments['past_appointment'].id,
            appointments['current_appointment'].id,
            appointments['future_appointment'].id,
            appointments['late_night_appointment'].id,
            appointments['next_day_appointment'].id,
        ]))
Ejemplo n.º 10
0
def get_date_with_lowest_price_on_current_week(
        stylist: Stylist,
        prices_on_dates: List[ClientPriceOnDate]) -> Optional[datetime.date]:
    """
    Find the lowest, non-repeating price on this week and return it if it's today or is
    in the future. Return None otherwise.

    Non-repeating means that we need indeed lowest price, e.g. 1.0 out of [1.0, 2.0, 3.0]
    If there's repeating low price, e.g. [1.0, 2.0, 1.0, 3.0] - then we will return None
    """

    # filter out prices of the remainder of this week only
    current_date = stylist.with_salon_tz(timezone.now()).date()
    weekday = current_date.isoweekday(
    ) % 7  # Sunday is the beginning of the week
    end_of_week_date = current_date + datetime.timedelta(days=6 - weekday)
    this_week_prices: List[ClientPriceOnDate] = list(
        filter(
            lambda price_on_date: current_date <= price_on_date.date <=
            end_of_week_date, prices_on_dates))
    # There may be a situation when we don't have enough data for this week; return None in
    # this case
    if len(this_week_prices) < 2:
        return None
    # we need to find the ultimate, non-repeating minimum price. Let's sort this week's
    # prices; the first element will be the minimum, but we must check if the second element
    # is equal to it price-wise (which would mean that there's no single lowest price)
    prices_on_days_sorted = sorted(
        this_week_prices, key=lambda price_on_date: price_on_date.price)
    if prices_on_days_sorted[0].price == prices_on_days_sorted[1].price:
        # there's no local minimum: there are at least 2 days with equally low price
        return None
    if prices_on_days_sorted[0].date < current_date:
        # the lowest price is already in the past
        return None
    return prices_on_days_sorted[0].date
Ejemplo n.º 11
0
 def get_has_picture_set(self, stylist: Stylist) -> bool:
     return stylist.get_profile_photo_url() is not None
Ejemplo n.º 12
0
 def get_has_personal_data(self, stylist: Stylist) -> bool:
     full_name = stylist.get_full_name()
     has_name = full_name and len(full_name) > 1
     salon: Optional[Salon] = getattr(stylist, 'salon')
     has_address = salon and salon.address
     return bool(has_name and stylist.user.phone and has_address)
Ejemplo n.º 13
0
    def test_get_today_appointments(self, stylist_data: Stylist):
        client = G(Client)
        current_appointment = G(Appointment,
                                client=client,
                                stylist=stylist_data,
                                datetime_start_at=pytz.timezone(
                                    stylist_data.salon.timezone).localize(
                                        datetime.datetime(2018, 5, 14, 13,
                                                          20)),
                                duration=datetime.timedelta(minutes=30))

        past_appointment = G(Appointment,
                             client=client,
                             stylist=stylist_data,
                             datetime_start_at=pytz.timezone(
                                 stylist_data.salon.timezone).localize(
                                     datetime.datetime(2018, 5, 14, 12, 20)),
                             duration=datetime.timedelta(minutes=30))

        future_appointment = G(Appointment,
                               client=client,
                               stylist=stylist_data,
                               datetime_start_at=pytz.timezone(
                                   stylist_data.salon.timezone).localize(
                                       datetime.datetime(2018, 5, 14, 14, 20)),
                               duration=datetime.timedelta(minutes=30))

        late_night_appointment = G(Appointment,
                                   client=client,
                                   stylist=stylist_data,
                                   datetime_start_at=pytz.timezone(
                                       stylist_data.salon.timezone).localize(
                                           datetime.datetime(
                                               2018, 5, 14, 23, 50)),
                                   duration=datetime.timedelta(minutes=30))

        next_day_appointment = G(Appointment,
                                 client=client,
                                 stylist=stylist_data,
                                 datetime_start_at=pytz.timezone(
                                     stylist_data.salon.timezone).localize(
                                         datetime.datetime(
                                             2018, 5, 15, 13, 20)),
                                 duration=datetime.timedelta(minutes=30))

        today_appointments = [
            a.id for a in stylist_data.get_today_appointments()
        ]

        assert (len(today_appointments) == 3)
        assert (current_appointment.id in today_appointments)
        assert (past_appointment.id not in today_appointments)
        assert (future_appointment.id in today_appointments)
        assert (late_night_appointment.id in today_appointments)
        assert (next_day_appointment.id not in today_appointments)

        today_appointments = [
            a.id
            for a in stylist_data.get_today_appointments(upcoming_only=False)
        ]

        assert (len(today_appointments) == 4)
        assert (current_appointment.id in today_appointments)
        assert (past_appointment.id in today_appointments)
        assert (future_appointment.id in today_appointments)
        assert (late_night_appointment.id in today_appointments)
        assert (next_day_appointment.id not in today_appointments)
Ejemplo n.º 14
0
 def test_get_weekday_discount_percent(self, stylist_data: Stylist):
     assert (stylist_data.get_weekday_discount_percent(
         Weekday.MONDAY) == 50)
     assert (stylist_data.get_weekday_discount_percent(
         Weekday.TUESDAY) == 0)
Ejemplo n.º 15
0
    def test_search_stylists(self, client, stylist_data: Stylist):
        location = stylist_data.salon.location
        G(Salon, location=location)
        stylist_data_2 = G(Stylist)
        client_data = G(Client)

        results = SearchStylistView._search_stylists('',
                                                     '',
                                                     location=location,
                                                     country='US',
                                                     client_id=client_data.id)
        assert (len(results) == 1)

        results = SearchStylistView._search_stylists('Fred',
                                                     'los altos',
                                                     location=location,
                                                     country='US',
                                                     client_id=client_data.id)
        assert (len(results) == 1)
        assert (results[0] == stylist_data)
        assert (results[0].preference_uuid is None)
        preference = G(PreferredStylist,
                       client=client_data,
                       stylist=stylist_data)

        results = SearchStylistView._search_stylists('mcbob fr',
                                                     'rilma',
                                                     location=location,
                                                     country='US',
                                                     client_id=client_data.id)
        assert (len(results) == 1)
        assert (results[0] == stylist_data)
        assert (results[0].preference_uuid == preference.uuid)

        results = SearchStylistView._search_stylists('mcbob fr',
                                                     'junk-address',
                                                     location=location,
                                                     country='US',
                                                     client_id=client_data.id)
        assert (len(results) == 0)
        salon = stylist_data_2.salon
        salon.location = location
        salon.country = 'US'
        salon.save()
        results = SearchStylistView._search_stylists(
            stylist_data_2.get_full_name(),
            '',
            location=location,
            country='US',
            client_id=client_data.id)
        assert (len(results) == 1)
        assert (results[0] == stylist_data_2)

        results = SearchStylistView._search_stylists('some-junk-text',
                                                     '',
                                                     location=location,
                                                     country='US',
                                                     client_id=client_data.id)
        assert (len(results) == 0)
        # Test with deactivated stylist
        stylist_data.deactivated_at = timezone.now()
        stylist_data.save()
        results = SearchStylistView._search_stylists('Fred',
                                                     'los altos',
                                                     location=location,
                                                     country='US',
                                                     client_id=client_data.id)
        assert (len(results) == 0)
Ejemplo n.º 16
0
def generate_client_pricing_hints(
        client: Client, stylist: Stylist,
        prices_on_dates: List[ClientPriceOnDate]) -> List[ClientPricingHint]:
    hints: List[ClientPricingHint] = []
    MIN_DAYS_BEFORE_LOYALTY_DISCOUNT_HINT = 3
    MIN_DAYS_BEFORE_DEAL_OF_WEEK = 2
    loyalty_discount: LoyaltyDiscountTransitionInfo = get_current_loyalty_discount(
        stylist, client)
    current_discount_percent: int = loyalty_discount.current_discount_percent
    # 1. Check if current loyalty discount is about to transition to lower level
    if loyalty_discount.current_discount_percent and loyalty_discount.transitions_at:
        days_before_discount_ends = (loyalty_discount.transitions_at -
                                     timezone.now()).days
        if days_before_discount_ends <= MIN_DAYS_BEFORE_LOYALTY_DISCOUNT_HINT:
            if 0 < loyalty_discount.transitions_to_percent < current_discount_percent:
                # loyalty discount is about to transition to different non-zero
                # percentage, which is less than current discount
                hints.append(
                    ClientPricingHint(
                        priority=1,
                        hint=('Your {current_discount}% loyalty discount '
                              'reduces to {next_discount}% on {date}.').format(
                                  current_discount=loyalty_discount.
                                  current_discount_percent,
                                  next_discount=loyalty_discount.
                                  transitions_to_percent,
                                  date=loyalty_discount.transitions_at.
                                  strftime('%b, %-d'))))
            # loyalty discount just ends
            elif loyalty_discount.transitions_to_percent == 0:
                hints.append(
                    ClientPricingHint(
                        priority=1,
                        hint=
                        'Book soon, your loyalty discount expires on {date}.'.
                        format(date=loyalty_discount.transitions_at.strftime(
                            '%b, %-d'))))
    # 2. Check if there's upcoming deal of week
    next_deal_of_week_date: Optional[
        datetime.date] = get_next_deal_of_week_date(stylist=stylist)
    if next_deal_of_week_date:
        days_before_deal_of_week = (
            next_deal_of_week_date -
            stylist.with_salon_tz(timezone.now()).date()).days
        if days_before_deal_of_week > MIN_DAYS_BEFORE_DEAL_OF_WEEK:
            hints.append(
                ClientPricingHint(
                    priority=2,
                    hint='Book the Deal of the Week this {weekday}.'.format(
                        weekday=next_deal_of_week_date.strftime('%A'))))
    # 3. Check if there's a local minimum of price between current now and the
    # end of the week
    lowest_price_date = get_date_with_lowest_price_on_current_week(
        stylist=stylist, prices_on_dates=prices_on_dates)
    if lowest_price_date is not None:
        hints.append(
            ClientPricingHint(priority=3,
                              hint='Best price this week is {weekday}.'.format(
                                  weekday=lowest_price_date.strftime('%A'))))
    # for now, we will only return the first element
    return hints[:1]
Ejemplo n.º 17
0
 def get_upcoming_appointments(self, stylist: Stylist):
     return AppointmentSerializer(stylist.get_today_appointments(),
                                  many=True).data
Ejemplo n.º 18
0
def generate_demand_list_for_stylist(
        stylist: Stylist, dates: List[datetime.date]) -> List[DemandOnDate]:
    """
    Generate list of NamedTuples with demand (0..1 float), is_fully_booked(boolean),
    is_working_day(boolean), for each date supplied. Demand is generated
    based on formula: appointment_count * service_time_gap / available_time_during_day.
    If resulting demand value is > 1, we set it to 1. If no time is available during
    a day (or stylist is just unavailable on particular date) demand value will also
    be equal to 1
    """
    weekday_available_times = get_weekday_available_times(stylist)

    time_gap = stylist.service_time_gap
    demand_list = []

    for date_index, date in enumerate(dates):
        midnight = stylist.with_salon_tz(
            datetime.datetime.combine(date, datetime.time(0, 0)))
        next_midnight = midnight + datetime.timedelta(days=1)
        work_day_duration, stylist_weekday_availability = weekday_available_times[
            date.isoweekday()]
        # if stylist has specifically marked date as unavailable - reflect it
        has_special_date_unavailable = stylist.special_available_dates.filter(
            date=date, is_available=False).exists()
        if has_special_date_unavailable:
            work_day_duration = datetime.timedelta(0)
            stylist_weekday_availability = None
        load_on_date_duration = stylist.appointments.filter(
            datetime_start_at__gte=midnight,
            datetime_start_at__lt=next_midnight,
        ).exclude(status__in=[
            AppointmentStatus.CANCELLED_BY_STYLIST,
            AppointmentStatus.CANCELLED_BY_CLIENT
        ]).count() * time_gap
        is_stylist_weekday_available: bool = (
            stylist_weekday_availability.is_available
            if stylist_weekday_availability else False)
        is_working_day: bool = is_stylist_weekday_available and not has_special_date_unavailable

        demand_on_date = (load_on_date_duration / work_day_duration
                          if work_day_duration > datetime.timedelta(0) else
                          COMPLETELY_BOOKED_DEMAND)
        # pricing experiment: add experimental pricing logic when discounts are gradually reduced
        # for dates which are closer to today:
        # Today: assume the demand is at least 75% even if it is lower
        # Tomorrow: assume the demand is at least 50% even if it is lower
        # The day after tomorrow: assume the demand is at least 25% even if it is lower
        # After that: use the real demand

        if date_index == 0:  # today
            demand_on_date = max(demand_on_date, 0.75)
        elif date_index == 1:  # tomorrow
            demand_on_date = max(demand_on_date, 0.5)
        elif date_index == 2:  # day after tomorrow
            demand_on_date = max(demand_on_date, 0.25)

        # similar to demand_on_date which calculates the demand on the whole day,
        # we also need to calculate the demand during working hours to determine `is_fully_booked`
        if is_working_day:
            weekday_start_time = stylist.with_salon_tz(
                datetime.datetime.combine(
                    date, stylist_weekday_availability.work_start_at))
            weekday_end_time = stylist.with_salon_tz(
                datetime.datetime.combine(
                    date, stylist_weekday_availability.work_end_at))

            load_on_working_date_duration = stylist.appointments.filter(
                datetime_start_at__gte=weekday_start_time,
                datetime_start_at__lt=weekday_end_time,
            ).exclude(status__in=[
                AppointmentStatus.CANCELLED_BY_STYLIST,
                AppointmentStatus.CANCELLED_BY_CLIENT
            ]).count() * time_gap

        else:
            load_on_working_date_duration = datetime.timedelta(seconds=0)

        demand_on_working_hours = (load_on_working_date_duration /
                                   work_day_duration
                                   if work_day_duration > datetime.timedelta(0)
                                   else COMPLETELY_BOOKED_DEMAND)

        is_fully_booked: bool = False

        # Technically, there may be a situation of overbooking, which may cause actual
        # demand be > 1; in this case we'll cast it to 1 manually
        if demand_on_date > 1:
            demand_on_date = 1

        if is_working_day and demand_on_working_hours == COMPLETELY_BOOKED_DEMAND:
            is_fully_booked = True

        demand = DemandOnDate(demand=demand_on_date,
                              is_fully_booked=is_fully_booked,
                              is_working_day=is_working_day)
        demand_list.append(demand)
    return demand_list
Ejemplo n.º 19
0
 def test_other_discounts_set(self, stylist_data: Stylist):
     assert (
         StylistProfileStatusSerializer(
             instance=stylist_data).data['has_other_discounts_set'] is False
     )
     stylist_data.first_time_book_discount_percent = 10
     stylist_data.save()
     assert (
         StylistProfileStatusSerializer(
             instance=stylist_data).data['has_other_discounts_set'] is True
     )
     stylist_data.first_time_book_discount_percent = 0
     stylist_data.rebook_within_1_week_discount_percent = 10
     stylist_data.save()
     assert (
         StylistProfileStatusSerializer(
             instance=stylist_data).data['has_other_discounts_set'] is True
     )
     stylist_data.first_time_book_discount_percent = 0
     stylist_data.rebook_within_1_week_discount_percent = 0
     stylist_data.save()
     G(
         StylistDateRangeDiscount, stylist=stylist_data,
         dates=DateRange(datetime.date(2018, 4, 8), datetime.date(2018, 4, 10))
     )
     assert (
         StylistProfileStatusSerializer(
             instance=stylist_data).data['has_other_discounts_set'] is True
     )
Ejemplo n.º 20
0
def build_appointment_preview_dict(
        stylist: Stylist, client: Optional[Client],
        preview_request: AppointmentPreviewRequest
) -> AppointmentPreviewResponse:
    service_items: List[AppointmentServicePreview] = []
    appointment: Optional[Appointment] = None
    status = AppointmentStatus.NEW
    if preview_request.appointment_uuid is not None:
        appointment = get_object_or_404(stylist.appointments,
                                        uuid=preview_request.appointment_uuid)
        status = appointment.status
    total_discount_percentage: int = 0
    if appointment:
        total_discount_percentage = appointment.total_discount_percentage
    for service_request_item in preview_request.services:
        appointment_service: Optional[AppointmentService] = None
        if appointment:
            appointment_service = appointment.services.filter(
                service_uuid=service_request_item['service_uuid']).last()
        service_client_price: Optional[Decimal] = service_request_item[
            'client_price'] if 'client_price' in service_request_item else None

        if appointment_service:
            if service_client_price:
                # client price (overriding current regular price) was provided. We will
                # temporarily (w/out committing to DB) replace regular price of the service
                # with client-supplied, and will re-calculate client_price (i.e. client-facing)
                # with existing appointment's discount
                appointment_service.set_client_price(service_client_price,
                                                     commit=False)
            # service already exists in appointment, and will not be recalculated,
            # so we should take it's data verbatim
            service_item = AppointmentServicePreview(
                uuid=appointment_service.uuid,
                service_uuid=appointment_service.service_uuid,
                service_name=appointment_service.service_name,
                regular_price=appointment_service.regular_price,
                client_price=appointment_service.client_price,
                duration=appointment_service.duration,
                is_original=appointment_service.is_original)
            if not total_discount_percentage:
                total_discount_percentage = appointment_service.discount_percentage
        else:
            # appointment service doesn't exist in appointment yet, and is to be added, so we
            # need to calculate the price for it.
            service: StylistService = stylist.services.get(
                uuid=service_request_item['service_uuid'])
            # We need to decide what we use for the base price. If client_price is supplied
            # we will use it as a base price. Otherwise, we will take base price from stylist's
            # service
            if not service_client_price:
                regular_price = service.regular_price
            else:
                regular_price = service_client_price
            # now when we know the base price, we need to calculate price with discount.
            # if the appointment we're previewing is based on the existing appointment -
            # we will just take discount percentage from it. Otherwise, we need to run
            # pricing calculation for given client and stylist on the given date, and see
            # if there is any discount there
            if not appointment:
                calculated_price = calculate_price_and_discount_for_client_on_date(
                    service=service,
                    client=client,
                    date=preview_request.datetime_start_at.date())
                client_price = Decimal(calculated_price.price)
                if not total_discount_percentage:
                    total_discount_percentage = calculated_price.discount_percentage
            else:
                client_price = calculate_price_with_discount_based_on_appointment(
                    regular_price, appointment)

            service_item = AppointmentServicePreview(
                uuid=None,
                service_uuid=service.uuid,
                service_name=service.name,
                regular_price=service.regular_price,
                client_price=client_price,
                duration=service.duration,
                is_original=True if not appointment else False)
        service_items.append(service_item)

    total_client_price_before_tax = sum(
        [s.client_price for s in service_items], Decimal(0))
    total_regular_price = sum([s.regular_price for s in service_items],
                              Decimal(0))
    appointment_prices: AppointmentPrices = calculate_appointment_prices(
        price_before_tax=total_client_price_before_tax,
        include_card_fee=preview_request.has_card_fee_included,
        include_tax=preview_request.has_tax_included,
        tax_rate=stylist.tax_rate,
        card_fee=stylist.card_fee)
    duration = stylist.service_time_gap

    conflicts_with = stylist.get_appointments_in_datetime_range(
        preview_request.datetime_start_at,
        preview_request.datetime_start_at,
        including_to=True,
        exclude_statuses=[
            AppointmentStatus.CANCELLED_BY_STYLIST,
            AppointmentStatus.CANCELLED_BY_CLIENT
        ])
    return AppointmentPreviewResponse(
        duration=duration,
        conflicts_with=conflicts_with,
        total_client_price_before_tax=appointment_prices.
        total_client_price_before_tax,
        grand_total=appointment_prices.grand_total,
        tax_percentage=float(stylist.tax_rate) * 100,
        card_fee_percentage=float(stylist.card_fee) * 100,
        total_tax=appointment_prices.total_tax,
        total_card_fee=appointment_prices.total_card_fee,
        has_tax_included=appointment_prices.has_tax_included,
        has_card_fee_included=appointment_prices.has_card_fee_included,
        services=service_items,
        stylist=stylist,
        datetime_start_at=preview_request.datetime_start_at,
        status=status,
        total_discount_percentage=total_discount_percentage,
        total_discount_amount=max(
            total_regular_price - total_client_price_before_tax, Decimal(0)))