diff --git a/Gemfile b/Gemfile index 35de4a7f0e..f62cebd1e7 100644 --- a/Gemfile +++ b/Gemfile @@ -6,8 +6,14 @@ group :test do gem 'rspec' gem 'simplecov', require: false, group: :test gem 'simplecov-console', require: false, group: :test + gem 'timecop' + gem 'twilio_mock' end group :development, :test do gem 'rubocop', '1.20' end + +gem 'twilio-ruby', '~> 5.66.2' +gem 'rack' +gem 'dotenv' diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..9ff36510b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,49 @@ GEM remote: https://rubygems.org/ specs: + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) ansi (1.5.0) ast (2.4.2) + crack (0.4.5) + rexml diff-lcs (1.4.4) docile (1.4.0) + dotenv (2.7.6) + 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) + hashdiff (1.0.1) + jwt (2.3.0) + multipart-post (2.1.1) + nokogiri (1.13.4-x86_64-darwin) + racc (~> 1.4) parallel (1.20.1) parser (3.0.2.0) ast (~> 2.4.1) + public_suffix (4.0.7) + racc (1.5.1) + rack (2.2.3) rainbow (3.0.0) regexp_parser (2.1.1) rexml (3.2.5) @@ -36,6 +72,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) @@ -48,16 +85,33 @@ GEM simplecov_json_formatter (0.1.3) terminal-table (3.0.1) unicode-display_width (>= 1.1.1, < 3) + timecop (0.9.5) + twilio-ruby (5.66.2) + faraday (>= 0.9, < 2.0) + jwt (>= 1.5, <= 2.5) + nokogiri (>= 1.6, < 2.0) + twilio_mock (0.4.6) + twilio-ruby (~> 5.3, >= 5.3.1) + webmock (~> 3.0, >= 2) unicode-display_width (2.0.0) + webmock (3.14.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS ruby DEPENDENCIES + dotenv + rack rspec rubocop (= 1.20) simplecov simplecov-console + timecop + twilio-ruby (~> 5.66.2) + twilio_mock RUBY VERSION ruby 3.0.2p107 diff --git a/README.md b/README.md index dbcb154e43..1404df6058 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,43 @@ -Takeaway Challenge -================== +# Takeaway Challenge + +- This program is designed to order dishes from takeaway shop. +- This program can show menu and suggest if dish is available. +- This program creates basket for the user, where you they can put or delete selected dishes. +- This program can check the total of order. +- This program can send sms when order confirmed with suggested delivered time. +- This program uses Twilio. + +--- + +## How to install the program + ``` - _________ - 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: +- Fork this repository +- Use "git clone" + your fork URL +``` + +--- + +## How to run the program + +```shell + +irb -r "./lib/order.rb" +``` + +--- + +## How to run the test + +```shell +rspec + +irb +``` + + +## User story ``` As a customer So that I can check if I want to order something @@ -45,39 +54,4 @@ I would like to check that the total I have been given matches the sum of the va 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 -``` - -* 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. - -* Advanced! (have a go if you're feeling adventurous): - * Implement the ability to place orders via text message. - -* 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. - -> :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. - -* 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 - - -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. - -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. - -Notes on Test Coverage ------------------- - -You can see your [test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) when you run your tests. +``` \ No newline at end of file diff --git a/lib/dishes.rb b/lib/dishes.rb new file mode 100644 index 0000000000..bf7045077d --- /dev/null +++ b/lib/dishes.rb @@ -0,0 +1,22 @@ +class Dishes + attr_reader :menu + + def initialize + @menu = [ + { name: "Diet (not really) Pizza", price: 5.99 }, + { name: "Make me fat Pizza", price: 6.99 }, + { name: "Easy-Peasy Pizza", price: 7.99 }, + { name: "Hold my beer Pizza", price: 8.99 }, + { name: "Once in a lifetime Pizza", price: 9.99 } + ] + end + + def show_dishes + @menu.each_with_index { |meal, index| puts "#{index + 1}. #{meal[:name]} £#{meal[:price]}" } + @menu + end + + def dish_available?(index) + @menu[index] != nil + end +end diff --git a/lib/main.rb b/lib/main.rb new file mode 100644 index 0000000000..cb7bf958dc --- /dev/null +++ b/lib/main.rb @@ -0,0 +1,10 @@ +require_relative 'text' +require 'twilio-ruby' +require 'dotenv' + +account_sid = ENV['ACCOUNT_SID'] +auth_token = ENV['AUTH_TOKEN'] +client = Twilio::REST::Client.new(account_sid, auth_token) + +text = Text.new(client) +text.send_message diff --git a/lib/order.rb b/lib/order.rb new file mode 100644 index 0000000000..57a67b975b --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,40 @@ +require_relative 'dishes' +require_relative 'text' + +class Order + attr_reader :basket, :menu, :dishes, :complete, :text + + def initialize + @basket = Array.new(0) + dishes = Dishes.new + @menu = dishes.menu + @dishes = dishes + @complete = false + @text = Text.new + end + + def select_dishes(index) + raise "Order already confirmed and can't be changed." if @complete + raise "Dish is not available. Please choose a different dish." if !dishes.dish_available?(index - 1) + @basket << @menu[index - 1] + end + + def delete_dishes(index) + raise "Order already confirmed and can't be changed." if @complete + @basket.delete(@menu[index - 1]) + @basket + end + + def check_total + total = 0 + @basket.each { |meal| total += meal[:price] } + return "£#{total}" + end + + def confirm_order + @complete = true + puts "Order complete." + text.send_message + return check_total + end +end diff --git a/lib/text.rb b/lib/text.rb new file mode 100644 index 0000000000..24480f390a --- /dev/null +++ b/lib/text.rb @@ -0,0 +1,32 @@ +require 'twilio-ruby' +require 'dotenv' + +class Text + TIME_FORMAT = "%k:%M" + + attr_reader :mobile + + def initialize + @mobile = ENV['CUST_NUMBER'] + end + + def send_message + account_sid = ENV['ACCOUNT_SID'] + auth_token = ENV['AUTH_TOKEN'] + client = Twilio::REST::Client.new(account_sid, auth_token) + + begin + client.messages.create( + body: "Thank you! Your order was placed and will be delivered before #{delivery_time}", + to: @mobile, + from: ENV['TWILIO_NUMBER'] + ) + puts message.sid + rescue + end + end + + def delivery_time + time = (Time.now + 60 * 60).strftime(TIME_FORMAT) + end +end diff --git a/spec/dishes_spec.rb b/spec/dishes_spec.rb new file mode 100644 index 0000000000..801b579c1c --- /dev/null +++ b/spec/dishes_spec.rb @@ -0,0 +1,31 @@ +require 'dishes' + +describe Dishes do + it "creates an instance of the class" do + expect(subject).to be_instance_of(Dishes) + end + + describe "#show_dishes" do + it "prints a list of dishes with prices" do + expect { subject.show_dishes }.to output(<<~output + 1. Diet (not really) Pizza £5.99 + 2. Make me fat Pizza £6.99 + 3. Easy-Peasy Pizza £7.99 + 4. Hold my beer Pizza £8.99 + 5. Once in a lifetime Pizza £9.99 + output + ).to_stdout + end + end + + describe "#dishes_available?" do + it "returns true when dishes available" do + expect(subject.dish_available?(1)).to eq(true) + end + + it "returns true when dishes available" do + expect(subject.dish_available?(10)).to eq false + end + end +end + diff --git a/spec/order_spec.rb b/spec/order_spec.rb new file mode 100644 index 0000000000..e8898a7ddf --- /dev/null +++ b/spec/order_spec.rb @@ -0,0 +1,90 @@ +require 'order' +require 'dishes' + +describe Order do + it "has an empty array for new order" do + expect(subject.basket).to eq Array.new(0) + end + + it "defines @complete to eq false" do + expect(subject.complete).to eq false + end + + describe "#select_dishes" do + it "returns an array of selected available dishes" do + expect { subject.select_dishes(1) }.to change { + subject.basket }.from(Array.new(0)).to([{name: "Diet (not really) Pizza", price: 5.99}]) + end + + it "raises an error when dish is not available" do + expect { subject.select_dishes(16) }.to raise_error("Dish is not available. Please choose a different dish.") + end + + it "returns an array of selected available dishes" do + expect { subject.select_dishes(5) }.to change { + subject.basket }.from(Array.new(0)).to([{ name: "Once in a lifetime Pizza", price: 9.99 }]) + end + + it "raises an error when order is complete" do + subject.confirm_order + expect { subject.select_dishes(1) }.to raise_error "Order already confirmed and can't be changed." + end + end + + describe "#delete_dishes" do + it "removes the dish from an array" do + subject.select_dishes(1) + expect(subject.delete_dishes(1)).to eq (Array.new(0)) + end + + it "removes the dish from an array and leaves other items in the basket" do + subject.select_dishes(1) + subject.select_dishes(5) + expect(subject.delete_dishes(1)).to eq ([{name: "Once in a lifetime Pizza", price: 9.99}]) + end + + it "raises an error when order is complete" do + subject.confirm_order + expect { subject.delete_dishes(1) }.to raise_error "Order already confirmed and can't be changed." + end + end + + describe "#check_total" do + it "checks the total and the sum of 1 item" do + subject.select_dishes(1) + expect(subject.check_total).to eq "£5.99" + end + + it "checks the total and the sum of 2 items" do + subject.select_dishes(1) + subject.select_dishes(5) + expect(subject.check_total).to eq "£15.98" + end + + it "checks the total and the sum of 0 items" do + expect(subject.check_total).to eq "£0" + end + end + + describe "#confirm_order" do + let(:text) { double :text } + + it "returns the true and confirms the order when the total is correct" do + subject.confirm_order + expect(subject.complete).to eq true + end + + it "puts message confirming the order" do + expect { subject.confirm_order }.to output(<<~output + Order complete. + output + ).to_stdout + end + + it "checks if method send message was called" do + allow(subject).to receive(:text).and_return(text) + expect(text).to receive(:send_message) + subject.confirm_order + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d899..986d1bb630 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ require 'simplecov' require 'simplecov-console' +require 'twilio_mock' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, @@ -14,4 +15,7 @@ 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 + config.after(:each) do + TwilioMock::Mocker.new.clean + end end diff --git a/spec/text_spec.rb b/spec/text_spec.rb new file mode 100644 index 0000000000..c500ea8401 --- /dev/null +++ b/spec/text_spec.rb @@ -0,0 +1,37 @@ +require 'timecop' +require 'text' +require 'twilio_mock' +require 'twilio-ruby' + +describe Text do + let(:client) { double :client } + subject(:text) { described_class.new } + + it "creates an instance of the class" do + expect(subject).to be_instance_of(Text) + end + + describe "#send_message" do + it 'creates a new message' do + Timecop.freeze(Time.parse("19:00")) do + twilio_messages = double(:twilio_messages) + expect(Twilio::REST::Client).to receive(:new).with(ENV['ACCOUNT_SID'], ENV['AUTH_TOKEN']).and_return(client) + expect(client).to receive(:messages).and_return(twilio_messages) + expect(twilio_messages).to receive(:create).with( + body: "Thank you! Your order was placed and will be delivered before 20:00", + to: ENV['CUST_NUMBER'], + from: ENV['TWILIO_NUMBER'] + ).and_return(true) + text.send_message + end + end + end + + describe "#delivery_time" do + it "returns delivery time - current time plus 1 hour" do + Timecop.freeze(Time.parse("13:00")) do + expect(text.delivery_time).to eq("14:00") + end + end + end +end