diff --git a/.gitignore b/.gitignore
index d1a1edf06f..f7ee7a7a6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
# Local cache of Rubocop remote config
.rubocop-*
+twilio.env
diff --git a/Gemfile b/Gemfile
index 35de4a7f0e..6eb28c6ec9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,10 +4,12 @@ ruby '3.0.2'
group :test do
gem 'rspec'
- gem 'simplecov', require: false, group: :test
- gem 'simplecov-console', require: false, group: :test
+ gem 'simplecov', require: false
+ gem 'simplecov-console', require: false
+ gem 'simplecov-shields-badge', require: false
end
group :development, :test do
gem 'rubocop', '1.20'
+ gem 'twilio-ruby'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 66064703c7..c71877ace7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,9 +5,39 @@ GEM
ast (2.4.2)
diff-lcs (1.4.4)
docile (1.4.0)
+ faraday (1.10.0)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.0)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.3)
+ multipart-post (>= 1.2, < 3)
+ faraday-net_http (1.0.1)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ jwt (2.3.0)
+ mini_portile2 (2.8.0)
+ multipart-post (2.1.1)
+ nokogiri (1.13.4)
+ mini_portile2 (~> 2.8.0)
+ racc (~> 1.4)
parallel (1.20.1)
parser (3.0.2.0)
ast (~> 2.4.1)
+ racc (1.6.0)
rainbow (3.0.0)
regexp_parser (2.1.1)
rexml (3.2.5)
@@ -36,6 +66,7 @@ GEM
rubocop-ast (1.11.0)
parser (>= 3.0.1.1)
ruby-progressbar (1.11.0)
+ ruby2_keywords (0.0.5)
simplecov (0.21.2)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -45,9 +76,15 @@ GEM
simplecov
terminal-table
simplecov-html (0.12.3)
+ simplecov-shields-badge (0.1.0)
+ simplecov (~> 0.15)
simplecov_json_formatter (0.1.3)
terminal-table (3.0.1)
unicode-display_width (>= 1.1.1, < 3)
+ twilio-ruby (5.66.2)
+ faraday (>= 0.9, < 2.0)
+ jwt (>= 1.5, <= 2.5)
+ nokogiri (>= 1.6, < 2.0)
unicode-display_width (2.0.0)
PLATFORMS
@@ -58,6 +95,8 @@ DEPENDENCIES
rubocop (= 1.20)
simplecov
simplecov-console
+ simplecov-shields-badge
+ twilio-ruby
RUBY VERSION
ruby 3.0.2p107
diff --git a/README.md b/README.md
index dbcb154e43..da0c31c8df 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,83 @@
-Takeaway Challenge
-==================
+# Takeaway
+
+[![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
+[![Coverage](./badge.svg)](https://github.com/lukestorey95/takeaway-challenge)
+
+This program allows the user to order a takeaway, and receive a confirmation text informing them when to expect delivery. It uses a takeaway controller, order system, menu that loads dishes from a .csv file and TwilioAPI to send the text messages.
+
+
+
+## Installation
+
```
- _________
- r== | |
- _ // | M.A. | ))))
- |_)//(''''': | |
- // \_____:_____.-------D )))))
- // | === | / \
- .:'//. \ \=| \ / .:'':./ )))))
- :' // ': \ \ ''..'--:'-.. ':
- '. '' .' \:.....:--'.-'' .'
- ':..:' ':..:'
-
- ```
-
-Instructions
--------
-
-* Feel free to use google, your notes, books, etc. but work on your own
-* If you refer to the solution of another coach or student, please put a link to that in your README
-* If you have a partial solution, **still check in a partial solution**
-* You must submit a pull request to this repo with your code by 9am Monday morning
-
-Task
------
-
-* Fork this repo
-* Run the command 'bundle' in the project directory to ensure you have all the gems
-* Write a Takeaway program with the following user stories:
+$ git clone https://github.com/lukestorey95/takeaway-challenge.git
+
+$ cd takeaway-challenge
+$ bundle
+
+$ source ./twilio.env
```
-As a customer
-So that I can check if I want to order something
-I would like to see a list of dishes with prices
-As a customer
-So that I can order the meal I want
-I would like to be able to select some number of several available dishes
+
-As a customer
-So that I can verify that my order is correct
-I would like to check that the total I have been given matches the sum of the various dishes in my order
+## Quickstart
-As a customer
-So that I am reassured that my order will be delivered on time
-I would like to receive a text such as "Thank you! Your order was placed and will be delivered before 18:52" after I have ordered
```
+$ irb
-* Hints on functionality to implement:
- * Ensure you have a list of dishes with prices
- * The text should state that the order was placed successfully and that it will be delivered 1 hour from now, e.g. "Thank you! Your order was placed and will be delivered before 18:52".
- * The text sending functionality should be implemented using Twilio API. You'll need to register for it. It’s free.
- * Use the twilio-ruby gem to access the API
- * Use the Gemfile to manage your gems
- * Make sure that your Takeaway is thoroughly tested and that you use mocks and/or stubs, as necessary to not to send texts when your tests are run
- * However, if your Takeaway is loaded into IRB and the order is placed, the text should actually be sent
- * Note that you can only send texts in the same country as you have your account. I.e. if you have a UK account you can only send to UK numbers.
+> Dir['./lib/*.rb'].each {|file| require file }
-* Advanced! (have a go if you're feeling adventurous):
- * Implement the ability to place orders via text message.
+> takeaway = Takeaway.new
-* A free account on Twilio will only allow you to send texts to "verified" numbers. Use your mobile phone number, don't worry about the customer's mobile phone.
+> takeaway.display_menu
-> :warning: **WARNING:** think twice before you push your **mobile number** or **Twilio API Key** to a public space like GitHub :eyes:
->
-> :key: Now is a great time to think about security and how you can keep your private information secret. You might want to explore environment variables.
+> takeaway.add_to_order({ pizza: 9.50 })
-* Finally submit a pull request before Monday at 9am with your solution or partial solution. However much or little amount of code you wrote please please please submit a pull request before Monday at 9am
+> takeaway.check_order
+> takeaway.place_order
+```
-In code review we'll be hoping to see:
+
-* All tests passing
-* High [Test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) (>95% is good)
-* The code is elegant: every class has a clear responsibility, methods are short etc.
+## Running Tests
-Reviewers will potentially be using this [code review rubric](docs/review.md). Referring to this rubric in advance will make the challenge somewhat easier. You should be the judge of how much challenge you want this at this moment.
+```
+$ rspec
-Notes on Test Coverage
-------------------
+# The integration test is pending as running it will public send a text (costs 6p per text)
+```
-You can see your [test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) when you run your tests.
+
+
+## My Process
+
+1. Break down user stories into objects, attributes and behaviour
+2. Feature test and note down errors/expected errors
+3. Write failing test that replicates errors
+4. Write the minimum code to make test pass
+5. Refactor and ensure tests still pass
+6. Repeat step 2 and ensure behaviour works as intended
+
+
+
+## User Stories
+
+```
+As a customer
+So that I can check if I want to order something
+I would like to see a list of dishes with prices
+
+As a customer
+So that I can order the meal I want
+I would like to be able to select some number of several available dishes
+
+As a customer
+So that I can verify that my order is correct
+I would like to check that the total I have been given matches the sum of the various dishes in my order
+
+As a customer
+So that I am reassured that my order will be delivered on time
+I would like to receive a text such as "Thank you! Your order was placed and will be delivered before 18:52" after I have ordered
+```
\ No newline at end of file
diff --git a/assets/dishes.csv b/assets/dishes.csv
new file mode 100644
index 0000000000..465bb54d56
--- /dev/null
+++ b/assets/dishes.csv
@@ -0,0 +1,6 @@
+name, price, available
+pizza, 9.50,true
+pasta, 8.20,true
+tiramisu, 4.50,false
+calamari, 7.80,true
+risotto, 6.90,true
\ No newline at end of file
diff --git a/badge.svg b/badge.svg
new file mode 100644
index 0000000000..b846227899
--- /dev/null
+++ b/badge.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/diagram.md b/diagram.md
new file mode 100644
index 0000000000..e9b4948434
--- /dev/null
+++ b/diagram.md
@@ -0,0 +1,71 @@
+# Diagram
+
+```
+Takeaway
+ @menu Menu
+ @current_order Order
+
+ display_menu
+ should instruct Menu to display available dishes
+
+ add_to_order(dishes)
+ when at least one item is chosen
+ when all the dishes match dishes on the Menu
+ should instruct
+
+ when not all the dishes match dishes on the Menu
+ should raise error
+
+ when no dishes are not provided
+ should raise error
+
+ place_order
+ when current_order and basket are not empty
+ should change current_order order_complete from false to true
+ should instruct Text to send_text
+
+ when current_order basket are empty
+ should raise error
+
+Order
+ @basket []
+ @order_total_price 0.00
+ @order_complete false
+ @order_placed_time Time
+
+ add_to_basket
+ should change basket by number of dishes
+
+ check_order_total_matches_item_total
+ when basket are not empty
+ should return list of basket, prices and order_total_price
+
+ when basket are empty
+ should raise error
+
+Menu
+ @dishes_menu [{ name: “”, price: 0.00, available: true}]
+
+ display_available_dishes
+ when dishes_menu contains available dishes
+ should return list of available dishes names and prices
+
+ when dishes_menu is empty
+ should raise error
+
+ import_dishes
+ when dishes are not empty
+ should change dishes_menu by at least 1
+
+ when dishes are empty
+ should raise error
+
+Text
+ ORDER_CONFIRMATION_MESSAGE
+
+ @client Twilio::REST::Client
+ @message_body “”
+
+ send_text
+ should instruct client with message_body and order_placed_time
+```
\ No newline at end of file
diff --git a/lib/menu.rb b/lib/menu.rb
new file mode 100644
index 0000000000..37235363c1
--- /dev/null
+++ b/lib/menu.rb
@@ -0,0 +1,50 @@
+require 'csv'
+
+class Menu
+ attr_reader :dishes, :available_dishes
+
+ def initialize(dishes_file = "./assets/dishes.csv")
+ @dishes = []
+ load_dishes(dishes_file)
+
+ @available_dishes = filter_dishes_by_available
+ end
+
+ def display_available_dishes
+ available_dishes
+ end
+
+ private
+
+ attr_writer :dishes, :available_dishes
+
+ def load_dishes(dishes_file)
+ CSV.foreach(dishes_file, headers: true, header_converters: :symbol) do |row|
+ name, price, available = row[:name], row[:price], row[:available]
+
+ @dishes << { name: name.to_sym, price: price.to_f, available: true?(available) }
+ end
+ end
+
+ def true?(available)
+ available == "true"
+ end
+
+ def filter_dishes_by_available
+ self.available_dishes = dishes.select do |dish|
+ dish[:available] == true
+ end
+
+ format_available_dishes
+ end
+
+ def format_available_dishes
+ formatted_available_dishes = {}
+
+ self.available_dishes = available_dishes.each do |dish|
+ formatted_available_dishes.merge!(dish[:name] => dish[:price])
+ end
+
+ formatted_available_dishes
+ end
+end
diff --git a/lib/order.rb b/lib/order.rb
new file mode 100644
index 0000000000..a3415522e1
--- /dev/null
+++ b/lib/order.rb
@@ -0,0 +1,44 @@
+class Order
+ attr_reader :basket, :complete_status, :basket_total
+
+ def initialize
+ @basket = []
+ @complete_status = false
+ end
+
+ def add_to_basket(dishes)
+ dishes.each { |dish| basket << dish }
+ calculate_basket_total
+ end
+
+ def display_basket_and_total
+ { basket => basket_total }
+ end
+
+ def complete_order(completion_time)
+ order_complete?
+ basket_empty?
+ self.complete_status = true
+ self.completion_time = completion_time
+ end
+
+ private
+
+ attr_writer :complete_status, :completion_time, :basket_total
+
+ def basket_empty?
+ fail "No dishes in basket!" if basket.empty?
+ end
+
+ def order_complete?
+ fail "Order already completed!" if complete_status == true
+ end
+
+ def calculate_basket_total
+ prices = []
+ basket.each do |item|
+ prices << item.values[0]
+ end
+ self.basket_total = prices.sum
+ end
+end
diff --git a/lib/takeaway.rb b/lib/takeaway.rb
new file mode 100644
index 0000000000..82e180408f
--- /dev/null
+++ b/lib/takeaway.rb
@@ -0,0 +1,46 @@
+class Takeaway
+ STANDARD_DELIVERY_TIMEFRAME = 3600
+
+ def initialize(
+ menu = Menu.new,
+ current_order = Order.new,
+ text = Text.new
+ )
+ @menu = menu
+ @current_order = current_order
+ @text = text
+ end
+
+ def display_menu
+ menu.display_available_dishes
+ end
+
+ def add_to_order(*dishes)
+ current_order.add_to_basket(dishes)
+ end
+
+ def check_order
+ current_order.display_basket_and_total
+ end
+
+ def place_order
+ current_order.complete_order(
+ completion_time = current_time)
+
+ text.send_completion_message(
+ expected_delivery_time = standard_delivery_time)
+ end
+
+ private
+
+ attr_reader :menu, :current_order, :text
+
+ def standard_delivery_time
+ time = current_time + STANDARD_DELIVERY_TIMEFRAME
+ time.strftime("%H:%M")
+ end
+
+ def current_time
+ Time.now
+ end
+end
diff --git a/lib/text.rb b/lib/text.rb
new file mode 100644
index 0000000000..bad69f5027
--- /dev/null
+++ b/lib/text.rb
@@ -0,0 +1,36 @@
+require 'twilio-ruby'
+
+class Text
+ ACCOUNT_SID = ENV['TWILIO_ACCOUNT_SID']
+ AUTH_TOKEN = ENV['TWILIO_AUTH_TOKEN']
+ FROM_NUMBER = ENV['FROM_NUMBER']
+ TO_NUMBER = ENV['TO_NUMBER']
+
+ def initialize(
+ client = Twilio::REST::Client.new(
+ ACCOUNT_SID, AUTH_TOKEN)
+ )
+ @client = client
+ end
+
+ def send_completion_message(completion_time)
+ completion_message = "Thanks for your order! " +
+ "Our chefs are already busy preparing your food, " +
+ "and then it will be on it's way to you in no time. " +
+ "Listen out for the doorbell around #{completion_time}"
+
+ public_send(completion_message)
+ end
+
+ private
+
+ attr_reader :client
+
+ def public_send(message)
+ new_text = client.messages.create(
+ body: message,
+ from: FROM_NUMBER,
+ to: TO_NUMBER
+ )
+ end
+end
diff --git a/spec/dishes_spec.csv b/spec/dishes_spec.csv
new file mode 100644
index 0000000000..2dabfc5d5f
--- /dev/null
+++ b/spec/dishes_spec.csv
@@ -0,0 +1,4 @@
+name, price, available
+pizza, 9.50,true
+pasta, 8.20,true
+tiramisu, 4.50,false
\ No newline at end of file
diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb
new file mode 100644
index 0000000000..ef1cd26d3e
--- /dev/null
+++ b/spec/integration_spec.rb
@@ -0,0 +1,11 @@
+describe 'integration test' do
+ xit 'should not raise error' do
+ expect {
+ takeaway = Takeaway.new
+ takeaway.display_menu
+ takeaway.add_to_order({ pizza: 9.50 })
+ takeaway.check_order
+ takeaway.place_order
+ }.not_to raise_error
+ end
+end
diff --git a/spec/menu_spec.rb b/spec/menu_spec.rb
new file mode 100644
index 0000000000..fe04c2ef53
--- /dev/null
+++ b/spec/menu_spec.rb
@@ -0,0 +1,18 @@
+require 'csv'
+
+describe Menu do
+ subject(:menu) { Menu.new('./spec/dishes_spec.csv') }
+
+ describe '#display_available_dishes' do
+
+ context 'when some dishes are unavailable' do
+ it 'should only return available dish names and prices' do
+ expect(menu.display_available_dishes).to include(:pizza => 9.5, :pasta => 8.2)
+ end
+
+ it 'should not return unavailable dishes' do
+ expect(menu.display_available_dishes).not_to include(:tiramisu => 4.5)
+ end
+ end
+ end
+end
diff --git a/spec/order_spec.rb b/spec/order_spec.rb
new file mode 100644
index 0000000000..93e8c6ec2d
--- /dev/null
+++ b/spec/order_spec.rb
@@ -0,0 +1,41 @@
+describe Order do
+ subject(:order) { Order.new }
+ dishes = [{ pizza: 9.50 }, { pasta: 8.20 }]
+
+ describe '#add_to_basket' do
+ it 'should change basket to include dishes' do
+ order.add_to_basket(dishes)
+ expect(order.basket).to eq(dishes)
+ end
+ end
+
+ describe '#display_basket_total' do
+ it 'should return basket dishes and prices and total' do
+ order.add_to_basket(dishes)
+ expect(order.display_basket_and_total).to eq(dishes => 17.70)
+ end
+ end
+
+ describe '#complete_order' do
+ context 'when basket contains dishes' do
+ before(:each) { order.add_to_basket(dishes) }
+
+ it 'should change order status to completed' do
+ expect { order.complete_order('time') }.to change {
+ order.complete_status
+ }.from(false).to(true)
+ end
+
+ it 'should not allow a completed order to be completed again' do
+ allow(order).to receive(:complete_status).and_return(true)
+ expect { order.complete_order('time') }.to raise_error "Order already completed!"
+ end
+ end
+
+ context 'when basket is empty' do
+ it 'should return error when basket is empty' do
+ expect { order.complete_order('time') }.to raise_error "No dishes in basket!"
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 252747d899..282e6374bb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -6,12 +6,23 @@
# Want a nice code coverage website? Uncomment this next line!
# SimpleCov::Formatter::HTMLFormatter
])
-SimpleCov.start
-
-RSpec.configure do |config|
- config.after(:suite) do
- puts
- puts "\e[33mHave you considered running rubocop? It will help you improve your code!\e[0m"
- puts "\e[33mTry it now! Just run: rubocop\e[0m"
- end
+SimpleCov.start do
+ add_filter 'spec'
+ require 'shields_badge'
+ SimpleCov.formatter = SimpleCov::Formatter::ShieldsBadge
end
+
+# module FormatterOverrides
+# def example_pending(_)
+# end
+
+# def dump_pending(_)
+# end
+# end
+
+# RSpec::Core::Formatters::DocumentationFormatter.prepend FormatterOverrides
+
+require 'text'
+require 'takeaway'
+require 'menu'
+require 'order'
diff --git a/spec/takeaway_spec.rb b/spec/takeaway_spec.rb
new file mode 100644
index 0000000000..0921b48851
--- /dev/null
+++ b/spec/takeaway_spec.rb
@@ -0,0 +1,47 @@
+describe Takeaway do
+ let(:menu) { instance_double('Menu') }
+ let(:order) { instance_double('Order') }
+ let(:text) { instance_double('Text') }
+
+ subject(:takeaway) { Takeaway.new(menu, order, text) }
+
+ describe '#display_menu' do
+ it 'should instruct menu to return available dishes' do
+ expect(menu).to receive(:display_available_dishes)
+ takeaway.display_menu
+ end
+ end
+
+ describe '#add_to_order' do
+ it 'should instruct current_order to add dishes to basket' do
+ expect(order).to receive(:add_to_basket)
+ dishes = ['pizza', 'pasta']
+ takeaway.add_to_order(dishes)
+ end
+ end
+
+ describe '#check_order' do
+ it 'should instruct current_order to return dishes, prices and total' do
+ expect(order).to receive(:display_basket_and_total)
+ takeaway.check_order
+ end
+ end
+
+ describe '#place_order' do
+ it 'should instruct current_order to complete' do
+ allow(order).to receive(:complete_order)
+ allow(text).to receive(:send_completion_message)
+
+ expect(order).to receive(:complete_order)
+ takeaway.place_order
+ end
+
+ it 'should instruct text to send_completion_message' do
+ allow(order).to receive(:complete_order)
+ allow(text).to receive(:send_completion_message)
+
+ expect(text).to receive(:send_completion_message)
+ takeaway.place_order
+ end
+ end
+end
diff --git a/spec/text_spec.rb b/spec/text_spec.rb
new file mode 100644
index 0000000000..496aaad70c
--- /dev/null
+++ b/spec/text_spec.rb
@@ -0,0 +1,12 @@
+describe Text do
+ let(:client) { instance_double('Client', messages: self) }
+
+ subject(:text) { Text.new(client) }
+
+ describe '#send_completion_message' do
+ it 'should instruct client to send text message' do
+ expect(client.messages).to receive(:create)
+ text.send_completion_message('time')
+ end
+ end
+end