diff --git a/README.md b/README.md index ffefda9..7e6f3f4 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,16 @@ docker-compose run web rake db:create docker-compose run web rake db:migrate ``` +# To run tests + +## Unit tests + +```sh +docker-compose run web rspec spec/lib --order rand -fd +``` + +## Integrated tests + +```sh +docker-compose run web rspec spec/gateways spec/presenters spec/controllers --order rand -fd +``` diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb new file mode 100644 index 0000000..59b4846 --- /dev/null +++ b/app/controllers/orders_controller.rb @@ -0,0 +1,21 @@ +require './lib/orders/create_order_usecase' + +class OrdersController < ActionController::Base + def create + customer_gateway = CustomerGatewayDatabase.new + product_gateway = ProductGatewayDatabase.new + order_gateway = OrderGatewayDatabase.new + presenter = Orders::CreateOrderPresenterJson.new + + CreateOrderUsecase.new(customer_gateway, product_gateway, order_gateway, presenter) + .execute(params[:customer_id], get_products_id_with_quantity) + + render presenter.response + end + + private + + def get_products_id_with_quantity + params[:products_id_with_quantity].map{ |p| {product_id: p[:product_id].to_i, quantity: p[:quantity].to_f} } + end +end diff --git a/app/gateways/customer_gateway_database.rb b/app/gateways/customer_gateway_database.rb new file mode 100644 index 0000000..d1b78b7 --- /dev/null +++ b/app/gateways/customer_gateway_database.rb @@ -0,0 +1,7 @@ +require './lib/customers/gateways' + +class CustomerGatewayDatabase < CustomerGateway + def customer_exists?(customer_id) + Customer.exists?(customer_id) + end +end diff --git a/app/gateways/order_gateway_database.rb b/app/gateways/order_gateway_database.rb new file mode 100644 index 0000000..ae38b8e --- /dev/null +++ b/app/gateways/order_gateway_database.rb @@ -0,0 +1,22 @@ +require './lib/orders/gateways' + +class OrderGatewayDatabase < OrderGateway + def save_order(customer_id, order_products) + order = Order.create(customer_id: customer_id) + + for order_product in order_products + OrderProduct.create( + product_id: order_product.product_id, + quantity: order_product.quantity, + price: order_product.price, + order_id: order.id + ) + end + + return order.id + end + + def get_orders_by_customer_id(customer_id) + return Order.where(customer_id: customer_id) + end +end diff --git a/app/gateways/product_gateway_database.rb b/app/gateways/product_gateway_database.rb new file mode 100644 index 0000000..c8f1a3a --- /dev/null +++ b/app/gateways/product_gateway_database.rb @@ -0,0 +1,14 @@ +require './lib/products/gateways' +require './lib/products/exceptions' + +class ProductGatewayDatabase < ProductGateway + def find_products_by_ids(products_ids) + products = Product.where(id: products_ids) + + products_ids_found = products.map(&:id) + products_ids_not_found = products_ids.select{ |product_id| products_ids_found.exclude?(product_id) } + raise ProductsNotFoundException.new(products_ids_not_found) if products_ids_not_found.length > 0 + + return products + end +end diff --git a/app/models/order.rb b/app/models/order.rb index 10281b3..732f18c 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -1,2 +1,3 @@ class Order < ApplicationRecord + has_many :order_products end diff --git a/app/models/product.rb b/app/models/product.rb index 35a85ac..d8a29ff 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -1,2 +1,5 @@ class Product < ApplicationRecord + def product_id + id + end end diff --git a/app/presenters/orders/create_order_presenter_json.rb b/app/presenters/orders/create_order_presenter_json.rb new file mode 100644 index 0000000..18281c3 --- /dev/null +++ b/app/presenters/orders/create_order_presenter_json.rb @@ -0,0 +1,50 @@ +require './lib/orders/presenters' + +module Orders + class CreateOrderPresenterJson < CreateOrderPresenter + def initialize + @status = :bad_request + @errors = [] + @order = {:order_id => nil, :total_price => nil} + end + + def show_error_customer_not_found + @status = :not_found + @errors += ['Customer not found.'] + end + + def show_error_product_not_found(product_id) + @status = :not_found + @errors += ['Product not found: ' + product_id.to_s] + end + + def show_order(order_id, total_price) + @status = :ok + @order[:order_id] = order_id + @order[:total_price] = total_price + end + + def response + { + 'status': @status, + 'json': create_json_response + } + end + + private + + def create_json_response + if @errors.empty? + if @order[:order_id].nil? + json = {} + else + json = @order + end + else + json = {:errors => @errors} + end + + json + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 787824f..1f78f87 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,3 @@ Rails.application.routes.draw do - # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + post '/orders/', to: 'orders#create' end diff --git a/docker-compose.yml b/docker-compose.yml index d87b9e7..a9d87f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,11 @@ version: '2' services: db: - image: postgres + image: postgres:9.6.2 + ports: + - '5432' volumes: - - ./tmp/db:/var/lib/postgresql/data + - /var/lib/postgresql/data web: build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' diff --git a/lib/customers/gateways.rb b/lib/customers/gateways.rb new file mode 100644 index 0000000..37a021c --- /dev/null +++ b/lib/customers/gateways.rb @@ -0,0 +1,3 @@ +class CustomerGateway + def customer_exists?(customer_id); end +end diff --git a/lib/orders/create_order_usecase.rb b/lib/orders/create_order_usecase.rb new file mode 100644 index 0000000..d031937 --- /dev/null +++ b/lib/orders/create_order_usecase.rb @@ -0,0 +1,60 @@ +require './lib/orders/structs' +require './lib/products/exceptions' +require './lib/orders/order_entity' + +class CreateOrderUsecase + def initialize(customer_gateway, product_gateway, order_gateway, create_order_presenter) + @customer_gateway = customer_gateway + @product_gateway = product_gateway + @order_gateway = order_gateway + @presenter = create_order_presenter + end + + def execute(customer_id, products_id_with_quantity) + return if not customer_exists?(customer_id) + + products, has_error = find_products(products_id_with_quantity) + return if has_error + + order_products = get_order_products(products, products_id_with_quantity) + total_price = OrderEntity.get_total_price(order_products) + + order_id = @order_gateway.save_order(customer_id, order_products) + @presenter.show_order(order_id, total_price.round(2)) + end + + private + + def customer_exists?(customer_id) + if not @customer_gateway.customer_exists?(customer_id) + @presenter.show_error_customer_not_found + return false + end + + return true + end + + def find_products(products_id_with_quantity) + begin + products_ids = products_id_with_quantity.map { |p| p[:product_id] } + return @product_gateway.find_products_by_ids(products_ids), false + rescue ProductsNotFoundException => ex + for product_id in ex.products_ids + @presenter.show_error_product_not_found(product_id) + end + + return [], true + end + end + + def get_order_products(products, products_id_with_quantity) + order_products = [] + + for product in products + quantity = products_id_with_quantity.select { |p| p[:product_id] == product.product_id }[0][:quantity] + order_products += [OrderProductStruct.new(product.product_id, quantity, product.price)] + end + + return order_products + end +end diff --git a/lib/orders/gateways.rb b/lib/orders/gateways.rb new file mode 100644 index 0000000..772fd75 --- /dev/null +++ b/lib/orders/gateways.rb @@ -0,0 +1,4 @@ +class OrderGateway + def save_order(customer_id, order_products); end + def get_orders_by_customer_id(customer_id); end +end diff --git a/lib/orders/order_entity.rb b/lib/orders/order_entity.rb new file mode 100644 index 0000000..1e9173d --- /dev/null +++ b/lib/orders/order_entity.rb @@ -0,0 +1,13 @@ +class OrderEntity + class << self + def get_total_price(order_products) + total_price = 0 + + for order_product in order_products + total_price += order_product.price * order_product.quantity + end + + return total_price + end + end +end diff --git a/lib/orders/presenters.rb b/lib/orders/presenters.rb new file mode 100644 index 0000000..9d99397 --- /dev/null +++ b/lib/orders/presenters.rb @@ -0,0 +1,10 @@ +class CreateOrderPresenter + def show_error_customer_not_found; end + def show_error_product_not_found(product_id); end + def show_order(order_id, total_price); end +end + +class ShowOrdersByCustomerPresenter + def show_orders(orders); end + def error_customer_not_found(customer_id); end +end diff --git a/lib/orders/show_orders_by_customer_usecase.rb b/lib/orders/show_orders_by_customer_usecase.rb new file mode 100644 index 0000000..0a3764f --- /dev/null +++ b/lib/orders/show_orders_by_customer_usecase.rb @@ -0,0 +1,28 @@ +require './lib/orders/structs' +require './lib/orders/order_entity' + +class ShowOrdersByCustomerUsecase + def initialize(customer_gateway, order_gateway, show_orders_by_customer_presenter) + @customer_gateway = customer_gateway + @order_gateway = order_gateway + @presenter = show_orders_by_customer_presenter + end + + def execute(customer_id) + if not @customer_gateway.customer_exists?(customer_id) + @presenter.error_customer_not_found(customer_id) + return + end + + orders = @order_gateway.get_orders_by_customer_id(customer_id) + response = ShowOrdersByCustomerResponse.new([]) + for order in orders + total_price = OrderEntity.get_total_price(order.order_products) + response.orders_response += [ + OrderResponse.new(order.order_id, order.customer_id, total_price, order.order_products) + ] + end + + @presenter.show_orders(response) + end +end diff --git a/lib/orders/structs.rb b/lib/orders/structs.rb new file mode 100644 index 0000000..01ae31f --- /dev/null +++ b/lib/orders/structs.rb @@ -0,0 +1,8 @@ +ProductStruct = Struct.new(:product_id, :name, :price) + +OrderProductStruct = Struct.new(:product_id, :quantity, :price) + +OrderGatewayStruct = Struct.new(:order_id, :customer_id, :order_products) + +ShowOrdersByCustomerResponse = Struct.new(:orders_response) +OrderResponse = Struct.new(:order_id, :customer_id, :total_price, :order_products) diff --git a/lib/products/exceptions.rb b/lib/products/exceptions.rb new file mode 100644 index 0000000..1049917 --- /dev/null +++ b/lib/products/exceptions.rb @@ -0,0 +1,8 @@ +class ProductsNotFoundException < StandardError + attr_reader :products_ids + + def initialize(products_ids) + @products_ids = products_ids + super + end +end diff --git a/lib/products/gateways.rb b/lib/products/gateways.rb new file mode 100644 index 0000000..069a62c --- /dev/null +++ b/lib/products/gateways.rb @@ -0,0 +1,3 @@ +class ProductGateway + def find_products_by_ids(products_ids); end +end diff --git a/spec/controllers/orders_controller_spec.rb b/spec/controllers/orders_controller_spec.rb new file mode 100644 index 0000000..a456d83 --- /dev/null +++ b/spec/controllers/orders_controller_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe OrdersController do + describe 'create order' do + it 'create order with products and customer' do + product = Product.create(price: 100.0) + customer = Customer.create() + products_id_with_quantity = [{product_id: product.id, quantity: 20}] + + post :create, as: :json, customer_id: customer.id, products_id_with_quantity: products_id_with_quantity, format: :json + + response_body = ActiveSupport::JSON.decode(response.body) + expect(response.status).to eq(200) + expect(response_body["order_id"]).to_not eq(nil) + expect(response_body["total_price"]).to eq("2000.0") + end + end +end diff --git a/spec/gateways/customer_gateway_database_spec.rb b/spec/gateways/customer_gateway_database_spec.rb new file mode 100644 index 0000000..46766aa --- /dev/null +++ b/spec/gateways/customer_gateway_database_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe CustomerGatewayDatabase do + let(:gateway) { described_class.new() } + + describe '#customer_exists?' do + context 'when customer exists' do + it 'returns true' do + customer = Customer.create() + + result = gateway.customer_exists?(customer.id) + + expect(result).to be true + end + end + + context 'when customer does not exists' do + it 'returns false' do + result = gateway.customer_exists?(123) + + expect(result).to be false + end + end + end +end diff --git a/spec/gateways/order_gateway_database_spec.rb b/spec/gateways/order_gateway_database_spec.rb new file mode 100644 index 0000000..1c94ca5 --- /dev/null +++ b/spec/gateways/order_gateway_database_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +RSpec.describe OrderGatewayDatabase do + let(:gateway) { described_class.new() } + let(:customer) { Customer.create() } + let(:product_one) { Product.create() } + let(:product_two) { Product.create() } + let(:order_products) { + [ + OrderProduct.new(product_id: product_one.id, quantity: 1, price: 10.0), + OrderProduct.new(product_id: product_two.id, quantity: 2, price: 20.0) + ] + } + + describe '#save_order' do + it 'save customer in order' do + gateway.save_order(customer.id, order_products) + + order = Order.first + expect(order.customer_id).to eq(customer.id) + end + + it 'save order and returns order_id' do + order_id = gateway.save_order(customer.id, order_products) + + order = Order.first + expect(order_id).to eq(order.id) + end + + it 'save all order_products' do + order_id = gateway.save_order(customer.id, order_products) + + order_products_database = OrderProduct.where(order_id: order_id) + expect(order_products_database.length).to eq(2) + + expect(order_products_database[0].product_id).to eq(order_products[0].product_id) + expect(order_products_database[0].quantity).to eq(order_products[0].quantity) + expect(order_products_database[0].price).to eq(order_products[0].price) + + expect(order_products_database[1].product_id).to eq(order_products[1].product_id) + expect(order_products_database[1].quantity).to eq(order_products[1].quantity) + expect(order_products_database[1].price).to eq(order_products[1].price) + end + end + + describe '#get_orders_by_customer_id' do + it 'filter by customer by id' do + Order.create(customer_id: customer.id) + + expect(gateway.get_orders_by_customer_id(999)).to eq([]) + end + + context 'when customer has not orders' do + it 'returns empty list' do + expect(gateway.get_orders_by_customer_id(customer.id)).to eq([]) + end + end + + context 'when customer has at least one order' do + it 'returns orders' do + order = Order.create(customer_id: customer.id) + order_products = [ + OrderProduct.create(order_id: order.id, product_id: product_one.id, quantity: 1, price: 10.0), + OrderProduct.create(order_id: order.id, product_id: product_two.id, quantity: 2, price: 15.0), + ] + + orders = gateway.get_orders_by_customer_id(customer.id) + + expect(orders.length).to eq(1) + expect(orders[0].order_products.length).to eq(2) + + expect(orders[0].customer_id).to eq(customer.id) + + expect(orders[0].order_products[0].product_id).to eq(order_products[0].product_id) + expect(orders[0].order_products[0].quantity).to eq(order_products[0].quantity) + expect(orders[0].order_products[0].price).to eq(order_products[0].price) + + expect(orders[0].order_products[1].product_id).to eq(order_products[1].product_id) + expect(orders[0].order_products[1].quantity).to eq(order_products[1].quantity) + expect(orders[0].order_products[1].price).to eq(order_products[1].price) + end + end + end + +end diff --git a/spec/gateways/product_gateway_database_spec.rb b/spec/gateways/product_gateway_database_spec.rb new file mode 100644 index 0000000..70c4c70 --- /dev/null +++ b/spec/gateways/product_gateway_database_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' +require './lib/products/exceptions' + +RSpec.describe ProductGatewayDatabase do + let(:gateway) { described_class.new() } + + describe '#find_products_by_ids' do + context 'when all products exists' do + it 'returns all products' do + product_one = Product.create() + product_two = Product.create() + product_not_searched = Product.create() + + result = gateway.find_products_by_ids([product_one.id, product_two.id]) + + expect(result).to match_array [product_one, product_two] + end + end + + context 'when at least one product does not exists' do + it 'raise an exception' do + product = Product.create() + + expect{ gateway.find_products_by_ids([product.id, 123]) }.to raise_error{ |error| + expect(error).to be_a(ProductsNotFoundException) + expect(error.products_ids).to match_array [123] } + end + end + end +end diff --git a/spec/lib/orders/create_order_usecase_spec.rb b/spec/lib/orders/create_order_usecase_spec.rb new file mode 100644 index 0000000..deca15c --- /dev/null +++ b/spec/lib/orders/create_order_usecase_spec.rb @@ -0,0 +1,94 @@ +require './lib/orders/create_order_usecase' +require './lib/products/gateways' +require './lib/orders/gateways' +require './lib/orders/presenters' +require './lib/customers/gateways' + +RSpec.describe CreateOrderUsecase do + let(:customer_gateway) { instance_double(CustomerGateway) } + let(:product_gateway) { instance_double(ProductGateway) } + let(:order_gateway) { instance_double(OrderGateway) } + let(:presenter) { instance_double(CreateOrderPresenter) } + let(:usecase) { described_class.new(customer_gateway, product_gateway, order_gateway, presenter) } + + context 'when an errors occurs' do + context 'customer not found' do + let(:customer_id) { 999 } + let(:products) { [] } + + before do + allow(customer_gateway).to receive(:customer_exists?).with(customer_id).and_return(false) + end + + it 'present customer not found' do + expect(presenter).to receive(:show_error_customer_not_found) + + usecase.execute(customer_id, products) + end + + it 'does not saves order' do + allow(presenter).to receive(:show_error_customer_not_found) + + expect(order_gateway).to_not receive(:save_order) + + usecase.execute(customer_id, products) + end + end + + context 'product not found' do + let(:customer_id) { 1 } + let(:products) { [{ product_id: 555, quantity: 1 }] } + + before do + allow(customer_gateway).to receive(:customer_exists?).with(customer_id).and_return(true) + allow(product_gateway).to receive(:find_products_by_ids).with([555]).and_raise(ProductsNotFoundException.new([555])) + end + + it 'present product not found' do + expect(presenter).to receive(:show_error_product_not_found).with(555) + + usecase.execute(customer_id, products) + end + + it 'does not saves order' do + allow(presenter).to receive(:show_error_product_not_found) + + expect(order_gateway).to_not receive(:save_order) + + usecase.execute(customer_id, products) + end + end + end + + context 'success' do + let(:customer_id) { 1 } + let(:products) { [{ product_id: 555, quantity: 2 }, { product_id: 666, quantity: 7 }] } + let(:product_one) { ProductStruct.new(555, 'product_one', 10.0) } + let(:product_two) { ProductStruct.new(666, 'product_two' ,8.4) } + let(:order_product_one) { OrderProductStruct.new(product_id=product_one.product_id, quantity=2, price=product_one.price) } + let(:order_product_two) { OrderProductStruct.new(product_id=product_two.product_id, quantity=7, price=product_two.price) } + + before do + allow(customer_gateway).to receive(:customer_exists?).with(customer_id).and_return(true) + allow(product_gateway).to receive(:find_products_by_ids).with([555, 666]).and_return([product_one, product_two]) + end + + it 'save order to customer with two products' do + allow(presenter).to receive(:show_order) + + expect(order_gateway).to receive(:save_order).with(customer_id, [order_product_one, order_product_two]) + + usecase.execute(customer_id, products) + end + + it 'present success with order_id and total_price' do + order_id = 22 + allow(order_gateway).to receive(:save_order).and_return(order_id) + + total_price = 78.8 + expect(presenter).to receive(:show_order).with(order_id, total_price) + + usecase.execute(customer_id, products) + end + end +end diff --git a/spec/lib/orders/show_orders_by_customer_usecase_spec.rb b/spec/lib/orders/show_orders_by_customer_usecase_spec.rb new file mode 100644 index 0000000..7ec8b22 --- /dev/null +++ b/spec/lib/orders/show_orders_by_customer_usecase_spec.rb @@ -0,0 +1,57 @@ +require './lib/customers/gateways' +require './lib/orders/structs' +require './lib/orders/presenters' +require './lib/orders/show_orders_by_customer_usecase' + +RSpec.describe ShowOrdersByCustomerUsecase do + let(:customer_gateway) { instance_double(CustomerGateway) } + let(:order_gateway) { instance_double(OrderGateway) } + let(:presenter) { instance_double(ShowOrdersByCustomerPresenter) } + let(:usecase) { described_class.new(customer_gateway, order_gateway, presenter) } + let(:customer_id) { 123 } + + context 'when customer has not orders' do + it 'presents empty list' do + allow(customer_gateway).to receive(:customer_exists?).with(customer_id).and_return(true) + allow(order_gateway).to receive(:get_orders_by_customer_id).with(customer_id).and_return([]) + + expect(presenter).to receive(:show_orders).with(ShowOrdersByCustomerResponse.new([])) + + usecase.execute(customer_id) + end + end + + context 'when customer does not exist' do + it 'presents error' do + allow(customer_gateway).to receive(:customer_exists?).with(customer_id).and_return(false) + + expect(presenter).to receive(:error_customer_not_found).with(customer_id) + + usecase.execute(customer_id) + end + end + + context 'when customer has at least one order' do + it 'presents orders' do + allow(customer_gateway).to receive(:customer_exists?).with(customer_id).and_return(true) + orders = [ + OrderGatewayStruct.new(456, customer_id, [ + OrderProductStruct.new(789, 6, 100), + OrderProductStruct.new(101, 3, 200), + ]) + ] + allow(order_gateway).to receive(:get_orders_by_customer_id).with(customer_id).and_return(orders) + + response = ShowOrdersByCustomerResponse.new([ + OrderResponse.new(456, customer_id, 1200, [ + OrderProductStruct.new(789, 6, 100), + OrderProductStruct.new(101, 3, 200), + ]) + ]) + expect(presenter).to receive(:show_orders).with(response) + + usecase.execute(customer_id) + end + end + +end diff --git a/spec/presenters/orders/create_order_presenter_json_spec.rb b/spec/presenters/orders/create_order_presenter_json_spec.rb new file mode 100644 index 0000000..8fe5272 --- /dev/null +++ b/spec/presenters/orders/create_order_presenter_json_spec.rb @@ -0,0 +1,85 @@ +RSpec.describe Orders::CreateOrderPresenterJson do + let(:presenter) { described_class.new() } + + describe '#respond' do + context 'when nothing is presented' do + it 'http status should be bad request' do + response = presenter.response + + expect(response[:status]).to eq(:bad_request) + end + + it 'response should be empty' do + response = presenter.response + + expect(response[:json]).to eq({}) + end + end + + context 'when customer not found is presented' do + before(:each) do + presenter.show_error_customer_not_found + end + + it 'http status should be not found' do + response = presenter.response + + expect(response[:status]).to eq(:not_found) + end + + it 'response with customer not found error' do + response = presenter.response + + expect(response[:json]).to eq({:errors => ['Customer not found.']}) + end + end + + context 'when product not found is presented' do + it 'http status should be not found' do + presenter.show_error_product_not_found(123) + + response = presenter.response + + expect(response[:status]).to eq(:not_found) + end + + it 'response product with error' do + presenter.show_error_product_not_found(123) + + response = presenter.response + + expect(response[:json]).to eq({:errors => ['Product not found: 123']}) + end + + it 'response products with error when two products presented with error' do + presenter.show_error_product_not_found(123) + presenter.show_error_product_not_found(478) + + response = presenter.response + + expect(response[:json]).to eq({:errors => ['Product not found: 123', 'Product not found: 478']}) + end + end + + context 'when order is presented' do + let(:order_id) { 890 } + let(:total_price) { 78912.78} + + before(:each) do + presenter.show_order(order_id, total_price) + end + + it 'http status should be ok' do + response = presenter.response + + expect(response[:status]).to eq(:ok) + end + + it 'response with order id and total_price' do + response = presenter.response + + expect(response[:json]).to eq({:order_id => order_id, :total_price => total_price}) + end + end + end +end