Extending Groser iOS and Rails apps to support grocery delivery in multiple cities - Part 2

2 minute read

This is Part 2 of multi part series about how I re-architect Rails and iOS apps to support grocery delivery in multiple cities to enable our new business model of selling SaaS solution to independent entrepreneurs wanting to create their own grocery delivery service. To understand rationale behind my decisions please read Part 1

Upgrading Client app search API

I use pg_search with scopes to search products by name. With ProductVariant I will need to narrow down Product search to specific store by ProductVariant. I will add Product.search scope, cause it will be used in multiple places.

class Product < ActiveRecord::Base
  scope :search, -> { |store_id, query| joins(:variants).where('product_variants.store_id = ? AND (product_variants.flags & 1 = 0 OR product_variants.flags IS NULL)', store_id).search_by_name(query).limit(30) }

  pg_search_scope :search_by_name,
                  :against => [:name], 
                  :using => {
                    :tsearch => { :normalization => 16, :dictionary => 'russian', :any_word => true, :prefix => true }, 
                    :trigram => { :threshold => 0.6 },

I’m sure it can be optimized further, but I want to get it done in shortest time possible, so querying Product ids and then selecting ProductVariant will do for now.

class Api::V2::SearchesController < Api::V2::ApiController
  respond_to :json

  def index
    raise ArgumentError.new("store_id must not be blank") if params[:store_id].blank?

    @products_grouped_by_shelf = []

    store_id = params[:store_id]
    query = params[:q].split.join(' ')

    product_ids = Product.search(store_id, query).pluck(:id)
    product_variants = ProductVariant.includes(:shelf).where(product_id: product_ids, store_id: store_id)

    @products_grouped_by_shelf = product_variants.group_by { |v| v.shelf }

    respond_with @products_grouped_by_shelf


Upgrading Shopper app search API

Next I need to upgrade shopper API which can search by Product name or barcode.

Since barcode can be universal as well as store specific (for bulk products), I query both Product.barcode and ProductVariant.barcode

class Product < ActiveRecord::Base
scope :search_barcode, -> { |store_id, barcode| joins(:variants).where('product_variants.store_id = ? AND (products.barcode = ? OR product_variants.barcode = ?)', store_id, barcode, barcode) }
class Api::V2::Shoppers::ProductsController < ApplicationController

  def index
    raise ArgumentError.new("store_id must be present") if params[:store_id].blank?
    raise ArgumentError.new("q or barcode param must be present") if params[:q].blank? and params[:barcode].blank?

    store_id = params[:store_id]

    if params[:q].present?
      products = Product.search(store_id, params[:q])
      products = Product.search_barcode(store_id, params[:barcode])

    product_variants = ProductVariant.where(product_id: products.map(&:id), store_id: store_id)

    render :json => product_variants.as_json(:only => [:store_price, :department_id])


Upgrading Order Dispatcher

Order dispatcher assigns incoming orders to available shoppers according to schedule set by shoppers. First, I need to query all active and available shoppers for the client’s city. I’ll delegate Order.city to associated User.

class Order < ActiveRecord::Base
  delegate :city, :to => :user

Next, I’ll add city query into shopper scopes, extracted into mixin.

module HasRoles
  extend ActiveSupport::Concern
  scope :active_shoppers, lambda { |city| where('roles_mask & ? = ? AND city_id = ?', mask_for([:shopper, :active]), mask_for([:shopper, :active]), city.id) }
  scope :admin_shoppers, lambda { |city| where('roles_mask & ? = ? AND city_id = ?', mask_for([:shopper, :admin]), mask_for([:shopper, :admin]), city.id) }

Now I can begin working on OrderDispatcher. First I’ll handle easy case of admin customer placing an order, and then general case.

class OrderDispatcher

  def self.dispatch(order)
    if order.user.is? :admin
      shopper = User.admin_shoppers(order.city).sample

    shoppers = User.active_shoppers(order.city)
                   .where(:schedule_hours => { active: true, deliveries_count: 0, blocked: false, starts_at: order.delivery_window_starts_at })
                   .select { |s| s.schedule_hours.count > 0 }


    shoppers_with_same_day_deliveries = 
      OrderDelivery.joins(:shopper, :delivery_option)
                   .where.not(shopper: nil, state: OrderDelivery.states[:canceled])
                   .where(:shopper => { city: city })
                   .where('DATE(delivery_options.window_starts_at) = ?', order.delivery_window_starts_at.to_date)
                   .select {|s| s.schedule_hours.where(active: true, deliveries_count: 0, blocked: false, starts_at: order.delivery_window_starts_at).count > 0 }


In Part 3 I will extend iOS app so that customers can change which city they are interested in and app will dynamically load Store with Shelves and Departments.