Stubhub | Senior | Coding
Anonymous User
2687
# My Notes:

- Make sure to ask if the timezone matters, e.g. do we need to send a batch of notifications
  at 12 PM Eastern Time and another batch at 12 PM Pacific Time?
- Ask clarifying questions, like what if there is a tie where two events happen at the same time. 
  What should you do in that scenario? Just take the first event.

"""
The solution you are writing is part of a marketing engine for our StubHub website.
You will be implementing the setup and business logic for different marketing campaigns that find “relevant” events for a user and notifies them accordingly.
We are first looking for the correctness and performance of your solution, but also the extensibility and long-term maintainability.

Q1
    This code is missing some key elements. Define the MarketingEngine class and implement a send_customer_notifications method to notify the customer of all the events happening in the same city as the customer.

    While in the real world we would notify the customer of these events via email or text, for the purposes of this exercise, we want you to output the text content we intend to send to the customer and print it to the console.

    Please note that this method will be invoked multiple times for multiple different customers.

Q2 
    Extend the solution to add a new campaign which sends a notification to the customer with the event closest to their next or upcoming birthday.

    Please note that this campaign will be run multiple times for multiple different customers.
    # Just pick the first event
    # 100_000 customers
    
Q3
    We now need to implement a new campaign that notifies the customer of the 5 closest events to the customer based on the distance between the customer and the event. 

    You should assume that we have about 10 million events and we need to call this per user.

Q4
    In the stubhub backend we have a simple restful api that you can call that will return prices for events:

    For all event prices: https://sh-mockapi.azurewebsites.net/api/ticketprice 

    For a price per event: https://sh-mockapi.azurewebsites.net/api/ticketprice?eventId={EventId} 

    i.e https://sh-mockapi.azurewebsites.net/api/ticketprice?eventId=1

    Can you call this service from your solution to send a notification of the 5 cheapest tickets within a Y mile radius of the customer? We can test with various radiuses.

"""

from bisect import bisect_left
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
from dateutil import parser
from math import sqrt
import requests
from typing import List

from requests.models import Response

class City:
    name: str
    x_cor: int
    y_cor: int
'''
/*-------------------------------------
        Coordinates are roughly to scale with miles in the USA

           2000 +----------------------+  
                |                      |  
                |                      |  
             Y  |                      |  
                |                      |  
                |                      |  
                |                      |  
                |                      |  
             0  +----------------------+  
                0          X          4000

        ---------------------------------------*/
'''

# Assume a static number of cities
CITY_MAP : dict[str, City]= {
    'New York': City('New York', 3572, 1455),
    'Los Angeles': City('Los Angeles', 462, 975),
    'Boston': City('Boston', 3778, 1566),
    'Chicago': City('Chicago', 2608, 1525),
    'San Francisco': City('San Francisco', 183, 1233),
    'Washington': City('Washington', 3358, 1320)
}

def distance(city1: City, city2: City) -> float:
    return sqrt(
        (city1.x_cor - city2.x_cor) * (city1.x_cor - city2.x_cor) + 
        (city1.y_cor - city2.y_cor) * (city1.y_cor - city2.y_cor)
    )

class Event:
    id: int
    name: str
    city: str
    event_date: datetime

class Customer:
    id: int
    name: str
    city: str
    birth_date: datetime

class MarketingEngine:
    def __init__(self, events: List[Event]):
        now: datetime = datetime.now()
        self.events: List[Event] = [
            event for event in events if event.event_date >= now
        ]
        self.city_to_events: dict[str, List[Event]] = defaultdict(list)
        for event in self.events:
            self.city_to_events[event.city].append(event)
        event_date_to_event: dict[datetime, Event] = {event.event_date: event for event in self.events}
        self.events_sorted_by_date = sorted(
            event_date_to_event.values(), key=lambda event: event.event_date
        )
        self.closest_threshold: int = 5

        self.prices_ttl: timedelta = timedelta(days=1)
        self.last_price_cache: datetime = datetime.now()
        price_data = requests.get("https://sh-mockapi.azurewebsites.net/api/ticketprice").json()
        self.id_to_event = {int(event["Id"]): event for event in price_data}


    def send_customer_notifications(self, customer: Customer) -> None:
        # "Sends" notifications via the print method
        print(f"Calling send_customer_notifications for customer {customer.name}")
        result = self.city_to_events.get(customer.city, [])
        print(result)
    
    def send_customer_notifications_by_birthday(self, customer: Customer) -> None:
        # log(|EVENTS|)
        if not self.events_sorted_by_date:
            print(f"No events for {customer}")
        base_birthdate = customer.birth_date
        now: datetime = datetime.now()
        next_birthday = datetime(now.year, base_birthdate.month, base_birthdate.day)
        if now > next_birthday:
            next_birthday = datetime(now.year + 1, next_birthday.month, next_birthday.day)
        print(f"{next_birthday=}")

        idx = bisect_left(self.events_sorted_by_date, next_birthday, key=lambda event: event.event_date)
        # If you're sandwiched between two events, which is closer?
        if idx > 0:
            time_diff_right = abs(self.events_sorted_by_date[idx].event_date - next_birthday)
            time_diff_left = abs(self.events_sorted_by_date[idx - 1].event_date - next_birthday)
            if time_diff_left <= time_diff_right:
                print(self.events_sorted_by_date[idx - 1])
                return
        print(self.events_sorted_by_date[idx])
    
    def send_customer_notifications_by_proximity(self, customer: Customer) -> None:
        customer_city = CITY_MAP.get(customer.city)
        if not customer_city:
            print(f"No city found for {customer}")
        result = []
        nearest_cities = sorted(CITY_MAP.values(), key=lambda city: distance(customer_city, city))
        for city in nearest_cities:
            events = self.city_to_events[city.name]
            for event in events:
                result.append(event)
                if len(result) == self.closest_threshold:
                    break
            if len(result) == self.closest_threshold:
                break
        print("Closest events: ")
        print(result)
    
    def part_4(self, customer: Customer, radius: int) -> None:
        customer_city = CITY_MAP.get(customer.city)
        if not customer_city:
            print(f"No city found for {customer}")
            return
        now = datetime.now()
        if now - self.last_price_cache < self.prices_ttl:
            price_data = requests.get("https://sh-mockapi.azurewebsites.net/api/ticketprice").json()
            # print(price_data)
            self.id_to_event = {int(event["Id"]): event for event in price_data}
            self.last_price_cache = now
        # 5 cheapest tickets in a y-mile radius
        cities_within_radius = [
            city for city in CITY_MAP.values() if distance(city, customer_city) <= radius
        ]
        event_ids_in_the_radius: List[int] = []
        for city in cities_within_radius:
            event_ids_in_the_radius.extend(event.id for event in self.city_to_events.get(city.name, []))
        print(f"{event_ids_in_the_radius=}")
        # List[(price, event)]
        prices_and_events = []
        for event_id in event_ids_in_the_radius:
            price = self.id_to_event[event_id]["Price"]
            prices_and_events.append((float(price), self.id_to_event[event_id]))
        prices_and_events.sort()
        if len(prices_and_events) < 5:
            prices_and_events = prices_and_events[:5]
        print([event for _, event in prices_and_events])


def main():
    events: list[Event] = [
        Event(1, "Phantom of the Opera", "New York", parser.parse("2023-12-23")), # X
        Event(2, "Metallica", "Los Angeles", parser.parse("2024-12-02")), # OK
        Event(3, "Metallica", "New York", parser.parse("2024-12-06")), # OK
        Event(4, "Metallica", "Boston", parser.parse("2024-10-23")), # OK
        Event(5, "LadyGaGa", "New York", parser.parse("2023-09-20")),
        Event(6, "LadyGaGa", "Boston", parser.parse("2024-08-01")), # OK
        Event(7, "LadyGaGa", "Chicago", parser.parse("2024-07-04")), # OK
        Event(8, "LadyGaGa", "San Francisco", parser.parse("2024-07-07")), #OK
        Event(9, "LadyGaGa", "Washington", parser.parse("2023-05-22")),
        Event(10, "Metallica", "Chicago", parser.parse("2024-01-01")),
        Event(11, "Phantom of the Opera", "San Francisco", parser.parse("2024-07-04")),
        Event(12, "Phantom of the Opera", "Chicago", parser.parse("2025-05-15")),
    ]

    customer: Customer = Customer(1, "Amos", "New York", parser.parse("1995-05-11"))
    
    engine: MarketingEngine = MarketingEngine(events)
    # engine.send_customer_notifications(customer)

    # engine.send_customer_notifications_by_birthday(customer)
    # print()
    # print("By proximity: ")
    # engine.send_customer_notifications_by_proximity(customer)

    engine.part_4(customer, 2000)
Comments (4)