ShockX on Youtube!
Github repo
For my second project at WeCode, I had the opportunity to clone-code StockX, my go-to site for buying and re-selling shoes. As a sneakerhead at heart, I was pumped to work on a clone-coding project for a website that I enjoyed using and was familiar with. Since I was the one who pitched the idea to clone-code StockX, I was selected as product manager of my team. I started the project off by sharing my account information with my group mates so that they could familiarize themselves with the different features of the website.
김민주
서유진
유승현
조혜윤
조수아
송빈호
김하성 (me!)
StockX isn't your typical ecommerce website. Shoes are listed for sale by users (not retailers), and users can place both bids and asks for shoes, similar to how people buy stocks online (hence the name StockX). The ask and bid prices for a particular shoe vary according to what the market deems is the shoe's overall resell value. If there's a matching ask and bid for a particular size of a shoe, an order is created and the seller ships the shoe to StockX, where the shoe is verified for authenticity and finally re-shipped to the buyer. Having bought and sold multiple shoes on StockX, I was all-too-familiar with this ask-bid model. However, analyzing this process from a data modelling perspective was definitely one of the hardest parts of this project.
Since shoes are released in different sets of sizes (Jordan 1 Turbo Green could have men's sizes 7-18 while Nike Kobe Protro 6 Grinch could have men's sizes 3.5-18), we created a product_sizes middle table that contained both the product_id and size_id. For asks and bids, we created separate asks and bids tables that contained the product_size_id, since users can only place a bid or ask for a particular size of a shoe. A separate orders table contained either an ask_id or bid_id (we assigned both ask_id and bid_id as nullable). Although we could have combined the asks and bids tables into one big "actions" table (since both tables are essentially the same), we decided to keep them separate in order to more easily differentiate between asks and bids when managing user orders and order statuses (current, pending, etc.).
StockX data modelling was notably different from other ecommerce sites because StockX doesn't have a cart feature. Users can either place asks or bids or can directly buy and sell a shoe by matching an existing ask or bid price.
For this project, I drafted the product/views.py, which included two separate classes, ProductListView and ProductDetailView. While both classes only had one HTTP method (GET), writing code for both features was tricky because of the different filter conditions involved. For ProductListView, I used Django's Q object to add different price filter conditions (lowest and highest prices). I also used Django's annotate feature to obtain the minimum ask for each shoe, since each shoe's minimum ask is displayed on StockX's product list view. Below is a snapshot of what the finalized code looked like:
price_condition = Q()
if lowest_price and highest_price:
price_condition.add(Q(min_price__gte=lowest_price) & Q(min_price__lte=highest_price) & Q(productsize__ask__order_status__name=ORDER_STATUS_CURRENT) & Q(productsize__size_id=size), Q.AND)
if lowest_price and not highest_price:
price_condition.add(Q(min_price__lte=lowest_price) & Q(productsize__ask__order_status__name=ORDER_STATUS_CURRENT) & Q(productsize__size_id=size), Q.AND)
if highest_price and not lowest_price:
price_condition.add(Q(min_price__gte=highest_price) & Q(productsize__ask__order_status__name=ORDER_STATUS_CURRENT) & Q(productsize__size_id=size), Q.AND)
if not highest_price and not lowest_price:
price_condition.add(Q(productsize__size_id=size), Q.AND)
products = Product.objects.annotate(min_price=Min('productsize__ask__price')).filter(price_condition)
total_products = [
{'productId' : product.id,
'productName' : product.name,
'productImage' : product.image_set.first().image_url,
'price' : min([int(ask.price) for ask in Ask.objects.filter(
product_size__product_id = product.id)]) if Ask.objects.filter(product_size__product_id = product.id) else 0 }
for product in products][offset:offset+limit]
Writing code for the detail view of each product (ProductDetailView) was tricky because of all of the information that had to calculated and sent to the frontend side. When a user clicks on different sizes of a particular shoe, information about the shoe that is displayed to the user (i.e. lowest_ask, highest_bid, last_sale, price_change_percentage, price_premium) changes according to the chosen size:
I used Django's lookup feature to navigate through the different table relationships of our data model and obtain the necessary values to calculate the different sales numbers for each size. Here's a snapshot of a portion of the finalized code:
product = Product.objects.get(id=product_id)
product_sizes = product.productsize_set.all()
sizes = Size.objects.filter(productsize__product__id=product.id)
results['sizes'] = [
{
'size_id' : product_size.size_id,
'size_name' : Size.objects.get(id=product_size.size_id).name,
'last_sale' : int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).last().price) \
if product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).exists() else 0,
'price_change' : int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).order_by('-matched_at')[0].price) \
- int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).order_by('-matched_at')[1].price) \
if product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY) else 0,
'price_change_percentage' : int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).order_by('-matched_at')[0].price) \
- int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).order_by('-matched_at')[1].price) \
if product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY) else 0,
'lowest_ask' : int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_CURRENT).order_by('price').first().price) \
if product_size.ask_set.filter(order_status__name=ORDER_STATUS_CURRENT) else 0,
'highest_bid' : int(product_size.bid_set.filter(order_status__name=ORDER_STATUS_CURRENT).order_by('-price').first().price) \
if product_size.bid_set.filter(order_status__name=ORDER_STATUS_CURRENT) else 0,
'total_sales' : product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).count(),
'price_premium' : int(100 * (int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).last().price) - int(product.retail_price)) \
/ int(product.retail_price)) if product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).last() else 0,
'average_sale_price' : int(product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).aggregate(total=Avg('price'))['total']) \
if product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY).exists() else 0,
'sales_history':
[
{
'sale_price' : int(ask.price),
'date_time' : ask.matched_at.strftime('%Y-%m-%d'),
'time' : ask.matched_at.strftime('%H:%m')
}
for ask in product_size.ask_set.filter(order_status__name=ORDER_STATUS_HISTORY)]
} for product_size in product_sizes]
Looking back, I could have definitely improved this code by using Django's select_related and prefetch_related features instead of repeatedly querying the database. This could have significantly improved the speed and efficiency with which I queried the database.
For ths project, I also wrote unit tests using python's unittest library to debug my code and check for errors. Writing unit tests for the first time was definitely challenging, and it took time away from working on new API endpoints for our project. At the end, I definitely realized the importance of unit tests for large projects where code quality assurance is key.
Hi, any chance you would be willing to connect and talk about taking this functionality and applying to a different product category?