Skip to content

Commit

Permalink
Customer - Added Orders APIs (#44)
Browse files Browse the repository at this point in the history
* Migration - OrderItems - Add shop_id

* Migration - Orders - Add deleted

* Customer - Orders - Added CRUD APIs
  • Loading branch information
ddlogesh authored Apr 20, 2022
1 parent e0f43ff commit 65e8115
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 7 deletions.
115 changes: 115 additions & 0 deletions app/controllers/api/order_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
class Api::OrderController < ApiController
before_action :load_order, only: [:update, :show, :item, :delete_item, :destroy]

def create
reason = {}
order = Customer.current.orders.new
%w(shipping_addr billing_addr).each { |key| order.send("#{key}=", params[key].permit(*Order::ADDRESS_PARAMS)) }

order.validate
reason = order.errors if order.errors.any?

order_items = order.order_items
params['items'].to_a.each do |item|
item = item.slice('item_id', 'item_variant_id', 'quantity').as_json
%w(item_id item_variant_id).each { |key| item[key] = ShortUUID.expand(item[key]) if item[key].present? }
order_item = order_items.new(item)
order_item.validate
reason = reason.merge(order_item.errors) if order_item.errors.any?
end

if reason.present?
render status: 400, json: { message: I18n.t('order.create_failed'), reason: reason }
return
end

ActiveRecord::Base.transaction do
order.save!(validate: false)
order_items.each { |order_item| order_item.save!(validate: false) }
end
render status: 200, json: { message: I18n.t('order.create_success'), data: { order: order.as_json } }
end

def index
params['page_size'] ||= LIMIT
filter_params = params['next_page_token'].present? ? JSON.parse(Base64.decode64(params['next_page_token'])) : params
filter_params = filter_params.slice(*%w(start_date end_date order_status payment_status sort_order page_size next_id))
filter_params['id'] = ShortUUID.expand(filter_params['id']) if filter_params['id'].present?

query = ValidateParam::Order.load_conditions(filter_params, { 'parent' => Customer.current })
if query.class == Hash
render status: 400, json: { message: I18n.t('order.fetch_failed'), reason: query }
return
end

query, data = query.preload(:order_items, :order_transactions), { orders: [] }
data = data.merge({ total: query.count, page_size: params['page_size'].to_i }) if params['next_page_token'].blank?

if params['next_page_token'].present? || data[:total] > 0
data[:orders] = query.limit(filter_params['page_size'].to_i + 1).map { |order| order.as_json }
if data[:orders].size > filter_params['page_size'].to_i
filter_params['next_id'] = ShortUUID.expand(data[:orders].pop['id'])
data[:next_page_token] = Base64.encode64(filter_params.to_json).gsub(/[^0-9a-z]/i, '')
end
end

render status: 200, json: { message: 'success', data: data }
end

def show
render status: 200, json: { message: 'success', data: { order: @order.as_json } }
end

def update
%w(order_status rating).each { |key| @order.send("#{key}=", params[key]) if params[key].present? }
%w(shipping_addr billing_addr).each { |key| @order.send("#{key}=", params[key].permit(*Order::ADDRESS_PARAMS)) if params[key].present? }

@order.validate
if @order.errors.any?
render status: 400, json: { message: I18n.t('order.update_failed'), reason: @order.errors }
return
end

@order.save!(validate: false)
render status: 200, json: { message: I18n.t('order.update_success'), data: { order: @order.as_json } }
end

def item
item_data = params.slice('item_id', 'item_variant_id', 'quantity').as_json
%w(item_id item_variant_id).each { |key| item_data[key] = ShortUUID.expand(item_data[key]) if item_data[key].present? }

order_item = @order.order_items.create(item_data)
if order_item.errors.any?
render status: 400, json: { message: I18n.t('order.update_failed'), reason: order_item.errors }
return
end

render status: 200, json: { message: I18n.t('order.update_success'), data: { order: @order.as_json } }
end

def delete_item
order_item = @order.order_items.find_by_id(ShortUUID.expand(params['item_id']))
if order_item.nil?
render status: 404, json: { message: I18n.t('item.not_found') }
return
end

order_item.destroy!
render status: 200, json: { message: I18n.t('order.update_success'), data: { order: @order.as_json } }
end

def destroy
@order.update!(deleted: true)
render status: 200, json: { message: I18n.t('order.delete_success') }
end

private

def load_order
@order = Customer.current.orders.undeleted.find_by_id(ShortUUID.expand(params['id']))
if @order.nil?
render status: 404, json: { message: I18n.t('validation.invalid_request'), reason: I18n.t('order.not_found') }
return
end
end
end
4 changes: 4 additions & 0 deletions app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ def is_blocked?
self.status != STATUSES['ACTIVE']
end

def is_active?
!self.deleted && self.status == STATUSES['ACTIVE']
end

def make_current
Thread.current[:customer] = self
end
Expand Down
4 changes: 4 additions & 0 deletions app/models/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def fetch_filterable_fields
self.meta['filterable_fields'].to_a.map { |reference_id, value| { 'reference_id' => reference_id, 'value' => value } }
end

def is_active?
!self.deleted && self.status_active?
end

private

def validations
Expand Down
30 changes: 29 additions & 1 deletion app/models/order.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
class Order < ApplicationRecord
enum order_status: { created: 1, placed: 2, delivery_pending: 3, delivered: 4, cancelled: 5 }, _prefix: true
enum payment_status: { pending: 1, completed: 2, failed: 3, refund_pending: 4, refund_failed: 5, refund_completed: 6 }, _prefix: true
END_ORDER_STATUSES = %w(delivered cancelled)
ADDRESS_PARAMS = %w(name street area city state pincode)

scope :undeleted, -> { where(deleted: false) }

belongs_to :shop, optional: true
belongs_to :customer, optional: true
has_many :order_items
has_many :order_transactions

validate :validations

def as_json
resp = super
resp['id'] = ShortUUID.shorten(self.id)
resp['items'] = self.order_items.as_json
resp['transactions'] = self.order_transactions.as_json
%w(billing_addr shipping_addr).each { |key| resp[key] = resp[key].sort.to_h if resp[key].present? }
%w(price rating tax).each { |key| resp[key] = resp[key].to_f }
%w(order_placed_time created_at updated_at).each { |key| resp[key] = resp[key].in_time_zone(PlatformConfig['time_zone']).strftime('%Y-%m-%d %H:%M:%S') if resp[key].present? }

return resp
return resp.sort.to_h
end

def calculate_price
self.price = self.order_items.sum(:total_price).to_f.round(2)
end

def set_shop _shop_id
self.shop_id = _shop_id
end

private

def validations
if self.order_status_placed?
errors.add(:order_items, I18n.t('validation.required', param: 'Item')) if self.order_items.empty?

self.order_placed_time = Time.now if self.order_status_was != 'placed'
end

errors.add(:rating, I18n.t('order.rating_not_allowed')) if self.rating_changed? && !END_ORDER_STATUSES.include?(self.order_status)
end
end
51 changes: 49 additions & 2 deletions app/models/order_item.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
class OrderItem < ApplicationRecord
belongs_to :order, optional: true
belongs_to :shop, optional: true
belongs_to :item, optional: true
belongs_to :item_variant, optional: true

validate :validations
before_save :set_attributes
after_create_commit :create_callbacks
after_update_commit :update_callbacks
after_destroy_commit :destroy_callbacks

def as_json
resp = super
resp = resp.except('order_id', 'created_at', 'updated_at')
%w(id item_id item_variant_id).each { |key| resp[key] = ShortUUID.shorten(self.send(key)) }
resp = resp.except('shop_id', 'order_id', 'created_at', 'updated_at')
%w(actual_price total_price).each { |key| resp[key] = resp[key].to_f }

return resp
return resp.sort.to_h
end

private

def validations
if self.item.nil? || !self.item.is_active?
errors.add(:item_id, I18n.t('item.not_found'))
elsif !self.item.shop.is_active?
errors.add(:shop_id, I18n.t('shop.not_found'))
end

errors.add(:item_variant_id, I18n.t('item.variant.not_found')) if self.item_variant.nil?
end

def set_attributes
self.item_name = self.item.name
self.shop = self.item.shop
self.item_variant_name = self.item_variant.variant_name
self.item_variant_value = self.item_variant.variant_value
self.actual_price = self.item_variant.actual_price.to_f
self.total_price = self.quantity * self.actual_price
end

def create_callbacks
self.commit_callbacks
end

def update_callbacks
self.commit_callbacks if self.saved_changes_to_total_price?
end

def destroy_callbacks
self.commit_callbacks
end

def commit_callbacks
self.order.calculate_price
self.order.set_shop(self.shop_id)
self.order.save!
end
end
2 changes: 1 addition & 1 deletion app/models/order_transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ def as_json
%w(amount).each { |key| resp[key] = resp[key].to_f }
%w(txn_time created_at updated_at).each { |key| resp[key] = resp[key].in_time_zone(PlatformConfig['time_zone']).strftime('%Y-%m-%d %H:%M:%S') if resp[key].present? }

return resp
return resp.sort.to_h
end
end
5 changes: 5 additions & 0 deletions app/models/shop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Shop < ApplicationRecord
has_many :deleted_conversations, -> { preload(:sender).where(purpose: Conversation::PURPOSES['SHOP_DELETE']) }, class_name: 'Conversation', as: :receiver
has_many :items
has_many :orders
has_many :order_items

validate :validations
after_create :add_shop_detail
Expand Down Expand Up @@ -66,6 +67,10 @@ def is_blocked?
self.status == STATUSES['BLOCKED']
end

def is_active?
!self.deleted && self.status == STATUSES['ACTIVE']
end

private

def validations
Expand Down
11 changes: 10 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ en:
delete_success: Shop is deleted successfully
description_long: Description should contain maximum of 250 characters
fetch_failed: Unable to fetch shops
blocked: "Your shop has been blocked. Please contact %{platform} team"
blocked: "Your shop is not active. Please contact %{platform} team"
item:
create_failed: Unable to register item
update_failed: Unable to update item
Expand All @@ -131,6 +131,15 @@ en:
delete_success: Item variant is removed successfully
name_not_allowed: "Item variant with %{variant_name} already exist"
already_exist: "Item variant with %{variant_name} - %{variant_value} already exist"
order:
not_found: Unable to find the order
create_failed: Unable to create order
create_success: Order is created successfully
update_failed: Unable to update order
update_success: Order is updated successfully
fetch_failed: Unable to fetch orders
delete_success: Order is removed successfully
rating_not_allowed: Please wait for the order to be completed to apply rating
ratelimit:
exceeded: You have exceeded the number of attempts. Please try again after sometime
login: You have exceeded the number of login attempts. Please try again after sometime
Expand Down
7 changes: 7 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
resources :shop, only: [:index, :show] do
resources :items, only: :index
end

resources :order, only: [:create, :index, :show, :update, :destroy] do
member do
post :item
delete 'item/:item_id', to: 'order#delete_item'
end
end
end
end

Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20220418163633_add_shop_id_to_order_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddShopIdToOrderItems < ActiveRecord::Migration[6.0]
def change
add_column :order_items, :shop_id, 'BIGINT'
end
end
5 changes: 5 additions & 0 deletions db/migrate/20220420165502_add_deleted_to_orders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddDeletedToOrders < ActiveRecord::Migration[6.0]
def change
add_column :orders, :deleted, :boolean, default: false
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_04_15_133730) do
ActiveRecord::Schema.define(version: 2022_04_20_165502) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -133,6 +133,7 @@
t.jsonb "meta", default: {}
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "shop_id"
end

create_table "order_transactions", id: :uuid, default: nil, force: :cascade do |t|
Expand Down Expand Up @@ -162,6 +163,7 @@
t.jsonb "meta", default: {}
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.boolean "deleted", default: false
end

create_table "platform_user_sessions", primary_key: "token", id: :string, force: :cascade do |t|
Expand Down
13 changes: 12 additions & 1 deletion lib/validate_param/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ def self.load_conditions params, options = {}
conditions, sort_order, errors = super(params, options)
return errors if errors.present?

query = ::Order.all
if params['order_status'].present?
order_statuses = Array.wrap(params['order_status']).map { |order_status| ::Order.order_statuses[order_status] }.compact
conditions << ActiveRecord::Base.sanitize_sql_array(["order_status IN (%s)", order_statuses.join(', ')]) if order_statuses.present?
end

if params['payment_status'].present?
payment_statuses = Array.wrap(params['payment_status']).map { |payment_status| ::Order.payment_statuses[payment_status] }.compact
conditions << ActiveRecord::Base.sanitize_sql_array(["payment_status IN (%s)", payment_statuses.join(', ')]) if payment_statuses.present?
end

query = options['parent'].orders.all
query = query.undeleted if params['include_deleted'] != 'true'
query = query.where(conditions.join(' AND ')) if conditions.present?

return query.order("id #{sort_order}")
Expand Down

0 comments on commit 65e8115

Please sign in to comment.