SHOCKX - Thoughts on clone-coding my go-to site for reselling shoes

김하성·2021년 4월 21일
0

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.

🙌 Frontend:

김민주
서유진
유승현

🙌 Backend:

조혜윤
조수아
송빈호
김하성 (me!)

🛠 Tools Used:

Frontend:

  • html
  • css
  • javascript
  • react
  • context API
  • git/github

Backend:

  • python
  • django
  • AQueryTool
  • MySQL
  • git/github
  • AWS
  • unittest

Project Workflow (SCRUM)

  • weekly SPRINT meetings
  • daily standup meetings
  • Trello for keeping track of work being done (backlog, this week, doing, and done)
  • Slack for open communication with team

Data Modelling (models.py)


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.

"ShockX" Main Features

User (signup and login)

  • Used pyjwt to create tokens to authorize user access to certain features of the website
  • Used a login decorator for user authorization (pyjwt)
  • Used Kakao API to enable user login via KakaoTalk
  • Users can view shoes that they already have in the "portfolio" section of their account

Product

  • Created overall product list view of shoes (Jordan brand only)
  • Created a filter component according to shoe size and price range (using query strings)
  • Incorporated pagination using limit and offset for the product list view of shoes (using query strings)
  • Created detailed view of each product (based on which product the user clicks on)

Order

  • Users can place an ask or bid for a particular size of a shoe
  • If there is a match between an ask and bid for a particular size of a shoe, then an order is created and the matching ask and bid prices are removed from the product detail view

What I Contributed:

  • product/views.py
  • product/urls.py
  • product/tests.py
  • models.py (group contribution)

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.

Key Takeaways:

  • Be sure to use Django's select_related and prefetch_related features to optimize database querying. By caching data that you're going to use repeatedly, you can reduce loading speed time and overall database expenditures.
  • Unit tests can actually increase development speed if used appropriately. For large-scale projects, running a bunch of unit tests at once is much faster than conducting integrated tests or API endpoint tests, which are prone to human errors and oversight. If you're looking to expand on previuosly written code, it's also easier to write new code and conduct new tests using the unit tests that you've already drafted.
  • Be more cognizant of how you're quering the database! SQL queries should be written with cost and loading speed in mind, since each hit to the database costs time and money.
profile
#mambamentality 🐍

1개의 댓글

comment-user-thumbnail
2022년 11월 9일

Hi, any chance you would be willing to connect and talk about taking this functionality and applying to a different product category?

답글 달기