Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
Implement order api
Browse files Browse the repository at this point in the history
  • Loading branch information
tyler-goodwin committed Jul 10, 2020
1 parent 1294e25 commit 9ce5008
Show file tree
Hide file tree
Showing 22 changed files with 484 additions and 36 deletions.
36 changes: 36 additions & 0 deletions backend/app/controllers/api/v1/orders_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class Api::V1::OrdersController < ApplicationController
def index
@orders = Order.all
render json: @orders
end

def create
@order = Order.new(order_params)

if @order.save
render json: {
message: 'Order Successfully Placed!',
order: @order
}
else
render json: {
message: 'Failed to place order.',
errors: []
}, status: :bad_request
end
end

def prices
@order = Order.new(order_params)
prices = OrderPriceService.new.calculate_prices(@order)
render json: prices
end

private

def order_params
params.require(:order).permit(
order_items_attributes: %i[product_id quantity]
)
end
end
2 changes: 1 addition & 1 deletion backend/app/controllers/api/v1/products_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Api::V1::ProductsController < ApplicationController
def index
products = ProductService.new.available_products
products = Product.all
render json: products
end
end
38 changes: 38 additions & 0 deletions backend/app/models/order.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class Order < ApplicationRecord
validates :shipping_price, numericality: { greater_than_or_equal_to: 0 }
validates :total_price, numericality: { greater_than: 0 }

has_many :order_items, dependent: :destroy, autosave: true
accepts_nested_attributes_for :order_items, reject_if: :all_blank

before_validation :update_prices
before_validation :ensure_items_present

def as_json(_options)
super(
include: {
order_items: {
only: %i[product_id quantity],
methods: %i[product_name unit_price]
}
}
)
end

private

def update_prices
prices = OrderPriceService.new.calculate_prices(self)
self.shipping_price = prices[:shipping_price]
self.total_price = prices[:total_price]
true
end

def ensure_items_present
errors.add(:order_items, 'must contain at least 1 item') unless total_qty.positive?
end

def total_qty
order_items.map(&:quantity).sum
end
end
12 changes: 12 additions & 0 deletions backend/app/models/order_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :product

default_scope { includes(:product) }

validates :order, :product, presence: true
validates :quantity, numericality: { greater_than: 0, only_integer: true }

delegate :name, to: :product, prefix: :product
delegate :unit_price, to: :product
end
4 changes: 4 additions & 0 deletions backend/app/models/product.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Product < ApplicationRecord
validates :name, presence: true
validates :unit_price, numericality: { greater_than: 0 }
end
20 changes: 20 additions & 0 deletions backend/app/services/order_price_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class OrderPriceService
def calculate_prices(order)
total_qty = 0
shipping_price = 30
total_price = 0

order.order_items.each do |item|
total_qty += item.quantity
total_price += item.quantity * item.unit_price
end

shipping_price = 0 if total_qty >= 10
total_price *= 0.9 if total_qty > 20

{
total_price: total_price + shipping_price,
shipping_price: shipping_price
}
end
end
20 changes: 0 additions & 20 deletions backend/app/services/product_service.rb

This file was deleted.

3 changes: 3 additions & 0 deletions backend/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace :api do
namespace :v1 do
get 'products' => 'products#index'
get 'orders' => 'orders#index'
post 'orders' => 'orders#create'
post 'orders/calculate-price' => 'orders#prices'
end
end
end
10 changes: 10 additions & 0 deletions backend/db/migrate/20200709103721_create_orders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateOrders < ActiveRecord::Migration[6.0]
def change
create_table :orders do |t|
t.integer :shipping_price, null: false
t.float :total_price, null: false

t.timestamps
end
end
end
10 changes: 10 additions & 0 deletions backend/db/migrate/20200710070619_create_products.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateProducts < ActiveRecord::Migration[6.0]
def change
create_table :products do |t|
t.string :name, null: false
t.float :unit_price, null: false

t.timestamps
end
end
end
11 changes: 11 additions & 0 deletions backend/db/migrate/20200710113332_create_order_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateOrderItems < ActiveRecord::Migration[6.0]
def change
create_table :order_items do |t|
t.references :order, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.integer :quantity, null: false

t.timestamps
end
end
end
28 changes: 27 additions & 1 deletion backend/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,35 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 0) do
ActiveRecord::Schema.define(version: 2020_07_10_113332) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

create_table "order_items", force: :cascade do |t|
t.bigint "order_id", null: false
t.bigint "product_id", null: false
t.integer "quantity", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["order_id"], name: "index_order_items_on_order_id"
t.index ["product_id"], name: "index_order_items_on_product_id"
end

create_table "orders", force: :cascade do |t|
t.integer "shipping_price", null: false
t.float "total_price", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

create_table "products", force: :cascade do |t|
t.string "name", null: false
t.float "unit_price", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

add_foreign_key "order_items", "orders"
add_foreign_key "order_items", "products"
end
14 changes: 7 additions & 7 deletions backend/db/seeds.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
# In lieu of a CMS for managing products, we will just seed the db
Product.create(
[
{ name: 'High Quality', unit_price: 20 },
{ name: 'Premium', unit_price: 30 }
]
)
51 changes: 51 additions & 0 deletions backend/test/controllers/api/v1/orders_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
class Api::V1::OrdersControllerTest < ActionDispatch::IntegrationTest
test 'should return expected orders in json format' do
expected = Order.all
get api_v1_orders_path
assert_response :ok
assert_equal expected.to_json, @response.body
end

test 'should create order' do
assert_difference 'Order.count', 1 do
post api_v1_orders_path, params: {
order_items_attributes: [
{ product_id: products(:one).id, quantity: 1 }
]
}, as: :json
end
assert_includes @response.body, products(:one).name
assert_response :ok
end

test 'should not create order without items' do
assert_no_difference 'Order.count', 1 do
post api_v1_orders_path, params: { order_items_attributes: [] }, as: :json
end
assert_response :bad_request
end

test 'should calculate price' do
expected = { 'shipping_price' => 30, 'total_price' => 40 }
post api_v1_orders_calculate_price_path, params: {
order_items_attributes: [
{ product_id: products(:one).id, quantity: 1 }
]
}, as: :json
json = JSON.parse(@response.body)
assert_equal expected['shipping_price'], json['shipping_price']
assert_equal expected['total_price'], json['total_price']
assert_response :ok
end

test 'should calculate price with no items' do
expected = { 'shipping_price' => 30, 'total_price' => 30 }
post api_v1_orders_calculate_price_path, params: {
order_items_attributes: []
}, as: :json
json = JSON.parse(@response.body)
assert_equal expected['shipping_price'], json['shipping_price']
assert_equal expected['total_price'], json['total_price']
assert_response :ok
end
end
9 changes: 2 additions & 7 deletions backend/test/controllers/api/v1/products_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

class Api::V1::ProductsControllerTest < ActionDispatch::IntegrationTest
test 'should return expected products in json format' do
expected = ProductService::Product.new('DummyProduct', 42)
ProductService
.any_instance
.stubs(:available_products)
.returns([expected])

expected = Product.all
get api_v1_products_path
assert_response :ok
assert_equal [expected].to_json, @response.body
assert_equal expected.to_json, @response.body
end
end
5 changes: 5 additions & 0 deletions backend/test/fixtures/order_items.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
item_one:
product: one
order: order_one
quantity: 1
4 changes: 4 additions & 0 deletions backend/test/fixtures/orders.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
order_one:
shipping_price: 30
total_price: 50
8 changes: 8 additions & 0 deletions backend/test/fixtures/products.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: "Test Product"
unit_price: 10

two:
name: "Test Product 2"
unit_price: 5
38 changes: 38 additions & 0 deletions backend/test/models/order_item_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require 'test_helper'

class OrderItemTest < ActiveSupport::TestCase
def setup
@order = Order.new
@product = products(:one)
@order_item = @order.order_items.build(
product: @product,
quantity: 1
)
end

test 'should be valid' do
assert @order_item.valid?
end

test 'should have order' do
@order_item.order = nil
assert_not @order_item.valid?
end

test 'should have product' do
@order_item.product = nil
assert_not @order_item.valid?
end

test 'should have quantity' do
@order_item.quantity = nil
assert_not @order_item.valid?
end

test 'should have positive quantity' do
@order_item.quantity = 0
assert_not @order_item.valid?
@order_item.quantity = -10
assert_not @order_item.valid?
end
end
Loading

0 comments on commit 9ce5008

Please sign in to comment.