diff --git a/.gitignore b/.gitignore index d1a1edf06f..37a4b9bd6e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ # Local cache of Rubocop remote config .rubocop-* +.env diff --git a/Gemfile b/Gemfile index 35de4a7f0e..f608f77ff7 100644 --- a/Gemfile +++ b/Gemfile @@ -10,4 +10,7 @@ end group :development, :test do gem 'rubocop', '1.20' + gem 'twilio-ruby' + gem 'dotenv' + end diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..92b05dc62f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,9 +5,40 @@ GEM ast (2.4.2) 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) + 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.5.1) rainbow (3.0.0) regexp_parser (2.1.1) rexml (3.2.5) @@ -36,6 +67,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 +80,22 @@ GEM 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 ruby DEPENDENCIES + dotenv rspec rubocop (= 1.20) simplecov simplecov-console + twilio-ruby RUBY VERSION ruby 3.0.2p107 diff --git a/README.md b/README.md index dbcb154e43..23739f6434 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Takeaway Challenge -================== +# Takeaway Challenge + ``` _________ r== | | @@ -12,22 +12,49 @@ Takeaway Challenge '. '' .' \:.....:--'.-'' .' ':..:' ':..:' - ``` +``` + +## How to install the program + +1. Run git clone https://github.com/S-Spiegl/takeaway-challenge-new +2. If you don't have bundler already, run the command gem install bundler +3. Run bundle-install + +How to use the program + +--- + +1. Navigate to the parent directory and open IRB +2. Enter: './lib/order.rb' +3. Create a new order (e.g. order = Order.new) +4. View the menu (e.g. order.view_menu) +5. Add items to your order using the order's (e.g. order.add(1)) +6. Check your total at any time (e.g. order.total) +7. Check your selection at any time (e.g. order.selection) +8. When you're ready to checkout, run order.checkout +9. Confirm that you're happy with your order and that it matches the total, then + run complete_order and provide your telephone number to receive a confirmation + (e.g order.complete_order(+1 23456 789 10123) +10. Enjoy! -Instructions -------- +How to test -* 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 ------ +Run rspec -* 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: +## 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: ``` As a customer @@ -47,37 +74,38 @@ 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. +- Hints on functionality to implement: -* Advanced! (have a go if you're feeling adventurous): - * Implement the ability to place orders via text message. + - 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. -* 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. +- 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 - +- 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. +- 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. +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 ------------------- +## 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. +test diff --git a/docs/review.md b/docs/review.md index b43def6b17..9004cf65f0 100644 --- a/docs/review.md +++ b/docs/review.md @@ -1,5 +1,6 @@ # Introduction -Welcome to the code review for Takeaway Challenge! Again, don't worry - you are not expected to have all the answers. The following is a code-review scaffold for Takeaway Challenge that you can follow if you want to. These are common issues to look out for in this challenge - but you may decide to take your own route. + +Welcome to the code review for Takeaway Challenge! Again, don't worry - you are not expected to have all the answers. The following is a code-review scaffold for Takeaway Challenge that you can follow if you want to. These are common issues to look out for in this challenge - but you may decide to take your own route. If you don't feel comfortable giving technical feedback at this stage, try going through this guide with your reviewee and review the code together. @@ -17,10 +18,10 @@ Please do include all the gems you use in your Gemfile. This is an important cou Every good code base will have its README updated following the [contribution notes](https://github.com/makersacademy/takeaway-challenge/blob/main/CONTRIBUTING.md), i.e. -* Make sure you have written your own README that briefly explains your approach to solving the challenge. -* If your code isn't finished it's not ideal but acceptable as long as you explain in your README where you got to and how you would plan to finish the challenge. +- Make sure you have written your own README that briefly explains your approach to solving the challenge. +- If your code isn't finished it's not ideal but acceptable as long as you explain in your README where you got to and how you would plan to finish the challenge. -The above is a relatively straightforward thing to do that doesn't involve much programming. Pro-tip: work on this while letting your sub-conscious work on those trickier coding problems :-) +The above is a relatively straightforward thing to do that doesn't involve much programming. Pro-tip: work on this while letting your sub-conscious work on those trickier coding problems :-) ## Instructions in README @@ -48,11 +49,11 @@ The README is a great place to show the full story of how your app is used (from 2.2.3 :010 > c.checkout(12.93) ``` -# Step 2: Tests and \*\_spec.rb files +# Step 2: Tests and \*\_spec.rb files ## Tests should test real behaviours not stubs -You may have read about ["Vacuous" tests](https://github.com/makersacademy/airport_challenge/blob/main/docs/review.md#avoid-vacuous-tests) in the airport challenge code review. The example there focused on how we shouldn't test the behaviour of a double; but we can get into similar trouble if we are stubbing a real object, e.g. +You may have read about ["Vacuous" tests](https://github.com/makersacademy/airport_challenge/blob/main/docs/review.md#avoid-vacuous-tests) in the airport challenge code review. The example there focused on how we shouldn't test the behaviour of a double; but we can get into similar trouble if we are stubbing a real object, e.g. ```ruby it 'sends a payment confirmation text message' do @@ -61,15 +62,15 @@ it 'sends a payment confirmation text message' do end ``` -In the above the `expect(subject).to receive(:send_sms)` command "stubs" out any existing method called `send_sms` on the subject. Using `expect` instead of `allow` means that at the end of the it block, RSpec checks that subject did receive the message `send_sms`, which we have ensured by calling `subject.send_sms`, so this test passes without ever touching the application code. +In the above the `expect(subject).to receive(:send_sms)` command "stubs" out any existing method called `send_sms` on the subject. Using `expect` instead of `allow` means that at the end of the it block, RSpec checks that subject did receive the message `send_sms`, which we have ensured by calling `subject.send_sms`, so this test passes without ever touching the application code. You can confirm this test is 'vacuous' by checking that the [test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) doesn't change when you remove it. -In general you shouldn't be stubbing out behaviour on the object under test. The two key exceptions are when you have randomness or a 3rd party API. We saw how to [stub random behaviour](https://github.com/makersacademy/airport_challenge/blob/main/docs/review.md#handling-randomness-in-tests) in the airport challenge code review, but how do we stub a 3rd party API? See the next section. +In general you shouldn't be stubbing out behaviour on the object under test. The two key exceptions are when you have randomness or a 3rd party API. We saw how to [stub random behaviour](https://github.com/makersacademy/airport_challenge/blob/main/docs/review.md#handling-randomness-in-tests) in the airport challenge code review, but how do we stub a 3rd party API? See the next section. ## Stubbing the Twilio API -The Twilio gem provides access to the online Twilio service. If we don't stub out this interaction, we will send test SMS messages every time we run our tests. Not a good thing. +The Twilio gem provides access to the online Twilio service. If we don't stub out this interaction, we will send test SMS messages every time we run our tests. Not a good thing. The simplest approach is to stub out a method that calls the service, for example: @@ -105,11 +106,11 @@ describe Takeaway end ``` -This ensures that Takeaway#complete_order gets some test coverage and that no SMS will be sent by our tests. This is acceptable, but we still don't have very good test coverage. See the pill on [levels of stubbing 3rd party services](https://github.com/makersacademy/course/blob/main/pills/levels_of_stubbing.md) for some alternatives. +This ensures that Takeaway#complete_order gets some test coverage and that no SMS will be sent by our tests. This is acceptable, but we still don't have very good test coverage. See the pill on [levels of stubbing 3rd party services](https://github.com/makersacademy/course/blob/main/pills/levels_of_stubbing.md) for some alternatives. ## Unit vs Integration tests -Note that if you create real objects (not doubles) in your unit tests other than that which is the subject, then you are using the [Chicago style](http://programmers.stackexchange.com/questions/123627/what-are-the-london-and-chicago-schools-of-tdd) of unit testing (also called [integration testing](http://stackoverflow.com/a/7876055/316729)). In general you want to separate up your unit from your integration (or "feature") tests. Unit tests can just rest in the root of the spec folder, but features of integration tests should go in a subfolder (spec/features or spec/integration) or even in a separate folder on the root directory to allow them to run completely separately. +Note that if you create real objects (not doubles) in your unit tests other than that which is the subject, then you are using the [Chicago style](http://programmers.stackexchange.com/questions/123627/what-are-the-london-and-chicago-schools-of-tdd) of unit testing (also called [integration testing](http://stackoverflow.com/a/7876055/316729)). In general you want to separate up your unit from your integration (or "feature") tests. Unit tests can just rest in the root of the spec folder, but features of integration tests should go in a subfolder (spec/features or spec/integration) or even in a separate folder on the root directory to allow them to run completely separately. At Makers Academy we recommend using the London style with doubles to effectively isolate the single class being tested in a unit test. @@ -160,7 +161,7 @@ end has two public methods, `complete_order` and `is_correct_amount?`, that both should be tested independently `is_correct_amount?` will be implicitly tested by any test of `complete_order`, but since it is public it should have it's own expilcit test. -However, perhaps `is_correct_amount?` will only ever used internally by Takeaway, and never called by any collaborator objects? In which case we can make the public interface of Takeaway simpler like so: +However, perhaps `is_correct_amount?` will only ever used internally by Takeaway, and never called by any collaborator objects? In which case we can make the public interface of Takeaway simpler like so: ```ruby class Takeaway @@ -178,23 +179,23 @@ class Takeaway end ``` -`private` methods do not require tests; although having too many private methods is a design smell. Anyhow, having moved `is_correct_amount?` into a private method we have reduced the complexity of the public interface of Takeaway (a good thing) and we no longer have to test `is_correct_amount?` explicitly, which also slims down our tests. Reducing the complexity of the public interface of any class (both in terms of number of methods, and numbers of arguments they take) is generally a good thing as it reduces the numbers of ways in which the object can interact with other objects, and encourages a loose coupling between objects that promotes re-use. +`private` methods do not require tests; although having too many private methods is a design smell. Anyhow, having moved `is_correct_amount?` into a private method we have reduced the complexity of the public interface of Takeaway (a good thing) and we no longer have to test `is_correct_amount?` explicitly, which also slims down our tests. Reducing the complexity of the public interface of any class (both in terms of number of methods, and numbers of arguments they take) is generally a good thing as it reduces the numbers of ways in which the object can interact with other objects, and encourages a loose coupling between objects that promotes re-use. # Step 3: Application code and \*.rb files ## Use of modules -There are two main uses of modules in Ruby; one is to provide 'utility' libraries (which are sometimes a code smell) and the other is to provide mixins. However, using a module as a mixin can violate the Single Responsibility Principle. Although code is _defined_ in the module, when it is `include`d in a class, its behaviour becomes part of that class and therefore part of the class's responsibilities. Shared behaviour can be refactored into mixins (e.g. `BikeContainer` in Boris Bikes), but other responsibilities the class is dependent on (e.g. sending text messages for the restaurant) should be injected (see [this practical on dependency injection](https://github.com/makersacademy/skills-workshops/blob/f5b4801840fe07d26ff70341652dc81dcda12289/practicals/object_oriented_design/dependency_injection.md)). +There are two main uses of modules in Ruby; one is to provide 'utility' libraries (which are sometimes a code smell) and the other is to provide mixins. However, using a module as a mixin can violate the Single Responsibility Principle. Although code is _defined_ in the module, when it is `include`d in a class, its behaviour becomes part of that class and therefore part of the class's responsibilities. Shared behaviour can be refactored into mixins (e.g. `BikeContainer` in Boris Bikes), but other responsibilities the class is dependent on (e.g. sending text messages for the restaurant) should be injected (see [this practical on dependency injection](https://github.com/makersacademy/skills-workshops/blob/f5b4801840fe07d26ff70341652dc81dcda12289/practicals/object_oriented_design/dependency_injection.md)). ## Law of Demeter The [Law of Demeter](https://en.wikipedia.org/wiki/Law_of_Demeter) suggests that: -* Each object should have only limited knowledge about other units: only units "closely" related to the current unit. -* Each object should only talk to its friends; don't talk to strangers. -* Only talk to your immediate friends. +- Each object should have only limited knowledge about other units: only units "closely" related to the current unit. +- Each object should only talk to its friends; don't talk to strangers. +- Only talk to your immediate friends. -The following test shows a process of reaching through a series of related objects. The warning sign is the multiple periods in `subject.menu.dishes.length`. Here we are seeing `Restaurant` is being tested for properties that belong to the menu - effectively they are none of restaurant's business and shouldn't be tested here; and we shouldn't see deep-reaching chains like this in application code either: +The following test shows a process of reaching through a series of related objects. The warning sign is the multiple periods in `subject.menu.dishes.length`. Here we are seeing `Restaurant` is being tested for properties that belong to the menu - effectively they are none of restaurant's business and shouldn't be tested here; and we shouldn't see deep-reaching chains like this in application code either: ```ruby describe Restaurant do @@ -204,11 +205,11 @@ describe Restaurant do end ``` -Note this is different to *method chaining*, which enables the calling of multiple methods _on the same object_ in one line of code. The Demeter violation applies to reaching down through multiple objects. +Note this is different to _method chaining_, which enables the calling of multiple methods _on the same object_ in one line of code. The Demeter violation applies to reaching down through multiple objects. ## Appropriate use of Dependency Injection -It is likely that the `Restaurant` (or equivalent) class is dependent on another object to handle the Twilio messaging. If not, then this is a violation of Single Responsibility Principle. In order to invert dependencies and make testing easier, the Twilio class should be _injected_ into the `Restaurant` class. So instead of: +It is likely that the `Restaurant` (or equivalent) class is dependent on another object to handle the Twilio messaging. If not, then this is a violation of Single Responsibility Principle. In order to invert dependencies and make testing easier, the Twilio class should be _injected_ into the `Restaurant` class. So instead of: ```ruby class Restaurant @@ -221,6 +222,7 @@ restaurant = Restaurant.new ``` You can have (note you can define a default for the dependency as shown here, but that's optional): + ```ruby class Restaurant def initialize(messager = Messager.new) @@ -236,9 +238,9 @@ restaurant = Restaurant.new(dummy_messager) ## Separation of Concerns -Applications generally comprise a number of *concerns*. For example, pure business logic is a concern; interacting with the user (UI) is a concern; persisting data to a file or database is a concern; and so on. Generally, as well as having a single responsibility, a class should only be involved in one concern (which kind of follows, right?). +Applications generally comprise a number of _concerns_. For example, pure business logic is a concern; interacting with the user (UI) is a concern; persisting data to a file or database is a concern; and so on. Generally, as well as having a single responsibility, a class should only be involved in one concern (which kind of follows, right?). -To this end, a class that contains pure business logic should not also be concerned with the User Interface or presentation logic. If your business logic class uses `puts` statements to communicate with the user, then it has poor separation of concerns. Business logic objects should return other objects and status indicators that can be translated in a separate presentation layer into user-friendly messages and interactions. This means our business logic is not constrained to a particular output representation. +To this end, a class that contains pure business logic should not also be concerned with the User Interface or presentation logic. If your business logic class uses `puts` statements to communicate with the user, then it has poor separation of concerns. Business logic objects should return other objects and status indicators that can be translated in a separate presentation layer into user-friendly messages and interactions. This means our business logic is not constrained to a particular output representation. Separation of concerns leads to some very powerful design patterns such as Model View Controller (MVC), which we will meet in Week 4. @@ -274,7 +276,7 @@ $ menu.display ## Design for Single Responsibility Principle -It's easy to overlook responsibilities and end up with a class that does too much. This is a great opportunity to refactor your design to extract those responsibilities. One common indication is that a group of methods share a noun. For example, in `Restaurant` we might have: +It's easy to overlook responsibilities and end up with a class that does too much. This is a great opportunity to refactor your design to extract those responsibilities. One common indication is that a group of methods share a noun. For example, in `Restaurant` we might have: ```ruby def add_to_order(item) @@ -290,17 +292,18 @@ def finalize_order end ``` -The noun 'order' appears in three method names and this is a clear indication that we need an `Order` class. The beauty of OO is that as soon as we extract this responsibility into another class, our design becomes instantly much more powerful. Enabling the restaurant to handle multiple orders is suddenly much easier. - +The noun 'order' appears in three method names and this is a clear indication that we need an `Order` class. The beauty of OO is that as soon as we extract this responsibility into another class, our design becomes instantly much more powerful. Enabling the restaurant to handle multiple orders is suddenly much easier. ## Personal details and tokens on GitHub A well ordered codebase will use ENV variables and the [dotenv gem](https://github.com/bkeepers/dotenv) to ensure that sensitive infomration such as phone numbers and security tokens are not pushed up to public repos on Github. ## Explore the language for solutions to common problems + ### Use `Hash.new` to specify defaults other than `nil` -This can be particularly useful if you are managing counts of things (e.g. dishes). Instead of: +This can be particularly useful if you are managing counts of things (e.g. dishes). Instead of: + ```ruby def initialize @items = {} @@ -311,7 +314,9 @@ def add_dish(dish, quantity = 1) @items[dish] += quantity end ``` + You can remove the test and initialization in the first line of `add_dish` by defining `0` as the default: + ```ruby def initialize @items = Hash.new(0) @@ -335,7 +340,9 @@ def total_price total end ``` + You can use the `reduce` method (alias `inject`) already provided by Ruby: + ```ruby def total_price @items.reduce { |sum, item| sum + item } @@ -344,9 +351,9 @@ end ## Open Closed Principle -The Open Closed Principle tells us that we want our code to be *open* for extension but *closed* for modification. The idea is that if we need to add some new functionality then we can do that by *extending* our code rather than *modifying* it. +The Open Closed Principle tells us that we want our code to be _open_ for extension but _closed_ for modification. The idea is that if we need to add some new functionality then we can do that by _extending_ our code rather than _modifying_ it. -For example the menu items should not be hard coded into a restaurant class. Arguably they should not be in the business logic at all, e.g. being added at runtime (i.e. in IRB) or loaded from an external hash or maybe even a file. We are concerned that the menu items will likely change, so if they are hard coded in like this: +For example the menu items should not be hard coded into a restaurant class. Arguably they should not be in the business logic at all, e.g. being added at runtime (i.e. in IRB) or loaded from an external hash or maybe even a file. We are concerned that the menu items will likely change, so if they are hard coded in like this: ```ruby class Restaurant @@ -358,7 +365,7 @@ class Restaurant end ``` -Then in order to make any changes to the menu we have to open up the Restaurant class itself. Restaurant is not open for extension at all in the above example. It must be opened up for modification in order to make any changes to the menu. Consider this alternative: +Then in order to make any changes to the menu we have to open up the Restaurant class itself. Restaurant is not open for extension at all in the above example. It must be opened up for modification in order to make any changes to the menu. Consider this alternative: ```ruby class Restaurant @@ -368,10 +375,10 @@ class Restaurant end ``` -The type of Menu that Restaurant uses can now easily be changed. It is "open for extension". We can create a new type of Menu class that loads from a file, takes dynamic user, whatever we want, and Restaurant will happily collaborate (assuming the menu has the correct public interface, e.g. Menu#contains?, Menu#price etc.). This latter Restaurant is also "closed for modification". We don't need to modify because it can easily be extended through the use of different collaborator objects. +The type of Menu that Restaurant uses can now easily be changed. It is "open for extension". We can create a new type of Menu class that loads from a file, takes dynamic user, whatever we want, and Restaurant will happily collaborate (assuming the menu has the correct public interface, e.g. Menu#contains?, Menu#price etc.). This latter Restaurant is also "closed for modification". We don't need to modify because it can easily be extended through the use of different collaborator objects. ## Use consistent styles and indentation -The Ruby community has a very consistent style guide and you should follow it. Use tools like [Rubocop](https://github.com/bbatsov/rubocop) (`gem install rubocop ; rubocop`) to analyze your code for violations. +The Ruby community has a very consistent style guide and you should follow it. Use tools like [Rubocop](https://github.com/bbatsov/rubocop) (`gem install rubocop ; rubocop`) to analyze your code for violations. You may find it difficult to remember the indentation rules, but one helpful rule of thumb for indentation is to ensure that you are two space indented inside any `do ... end`, `class ... end` or `def ... end` block. diff --git a/lib/menu.rb b/lib/menu.rb new file mode 100644 index 0000000000..62e0a476a8 --- /dev/null +++ b/lib/menu.rb @@ -0,0 +1,17 @@ +class Menu + + attr_reader :items + + def initialize + @items = [{ item_number: 1, scaldy: "hot pea", price: 4.00, available: true }, + { item_number: 2, scaldy: "hot tomato", price: 4.25, available: true }, + { item_number: 3, scaldy: "matzo ball", price: 5.00, available: false }, + { item_number: 4, scaldy: "hot potato", price: 3.75, available: true} + ] + end + + def view_menu + @items + end + +end diff --git a/lib/order.rb b/lib/order.rb new file mode 100644 index 0000000000..4c980583b2 --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,51 @@ +require_relative 'menu' +require_relative 'SMS' + +class Order + + attr_reader :selection, :items, :total + + def initialize(items = Menu.new.items) + @selection = [] + @items = items + @total = 0 + end + + def view_menu + @items + end + + def add(item_index) + + fail 'item not available' if @items[item_index - 1][:available] == false + @selection << @items[item_index - 1] + @total += @items[item_index - 1][:price] + @selected_item = @items[item_index - 1][:scaldy] + item_added_confirmation + end + + def checkout + p check_order_prompt, @selection, total_summary + # checkout_confirmation + end + + def check_order_prompt + "Please check your order against your total:" + end + + def selection_summary + @selection + end + + def total_summary + "Your total is: £#{@total}. Please use the complete_order function, entering your phone number as an argument, to complete your order" + end + + def item_added_confirmation + "You successfully added #{@selected_item} to your basket" + end + + def complete_order(phone_number) + SMS.new.send_sms(phone_number) + end +end diff --git a/lib/sms.rb b/lib/sms.rb new file mode 100644 index 0000000000..ada314c82e --- /dev/null +++ b/lib/sms.rb @@ -0,0 +1,40 @@ + +require 'twilio-ruby' +require 'dotenv/load' + +class SMS + + ETA = (Time.now + 3600).strftime("%H:%M") + + attr_reader :sent, :phone_number + + def initialize + @sent = false + end + + def sms + + @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'] + message = @client.messages.create( + body: "Thank you! Your order has been logged and will be with before #{ETA}", + to: "+#{@phone_number}", + from: ENV['TWILIO_PHONE']) + + puts message.sid + end + + def send_sms(phone_number) + @phone_number = phone_number + sms + @sent = true + sms_sent_confirmation + end + + def sent? + @sent + end + + def sms_sent_confirmation + "A confirmation message has been sent to the number you provided" + end +end diff --git a/spec/menu_spec.rb b/spec/menu_spec.rb new file mode 100644 index 0000000000..31ae4735f0 --- /dev/null +++ b/spec/menu_spec.rb @@ -0,0 +1,8 @@ +require 'menu' +describe Menu do + + it 'is an instance of menu' do + expect(subject).to be_instance_of(Menu) + + end +end diff --git a/spec/order_spec.rb b/spec/order_spec.rb new file mode 100644 index 0000000000..3c778c2321 --- /dev/null +++ b/spec/order_spec.rb @@ -0,0 +1,63 @@ +require 'order' + +describe Order do + + describe '#add dishes' do + + context 'when a customer selects an available dish' do + it 'it should change the order by adding a dish to it' do + expect { subject.add(1) }.to change(subject, :selection) + end + end + + context 'when a customer selects an unavailable dish' do + it 'it should not let them pick a dish' do + expect { subject.add(3) }.to raise_error 'item not available' + end + end + + context 'when a customer selects a meal' do + it 'it should add the cost of that dish to the total' do + expect { subject.add(1) }.to change(subject, :total) + end + end + + context 'when a customer selects a dish' do + it 'it should change the confirmation message accordingly' do + expect { subject.add(1) }.to change(subject, :item_added_confirmation) + end + end + + describe '#summary' do + context 'when a customer has finished choosing meals and clicks on checkout' do + it 'should show the customer their summary' do + subject.add(1) + expect(subject.checkout).to include(subject.selection) + end + + it 'should prompt the customer to compare the total against the order summary' do + subject.add(1) + expect(subject.checkout).to include("Please check your order against your total:") + end + end + end + end + + describe '#complete_order' do + context 'when a customer has checked their summary and enters complete_order' do + it 'should call the SMS class' do + subject.add(1) + subject.checkout + expect(subject).to respond_to(:complete_order).with(1).argument + end + end + + context 'when a customer has checked their summary and enters complete_order' do + it 'should be instance of SMS' do + subject.add(1) + subject.checkout + expect(subject.complete_order).to respond_to(SMS.new.send_sms) + end + end + end +end diff --git a/spec/sms_spec.rb b/spec/sms_spec.rb new file mode 100644 index 0000000000..f3d0ec055e --- /dev/null +++ b/spec/sms_spec.rb @@ -0,0 +1,27 @@ +describe SMS do + + subject(:sms) { described_class.new } + + before do + allow(sms).to receive(:sms) + end + + it 'sends a payment confirmation text message' do + expect(sms).to receive(:sms) + sms.send_sms(ENV['MY_PHONE']) + end + + describe '#send_sms' do + it "takes a phone number as an argument" do + expect(subject).to respond_to(:send_sms).with(1).argument + end + end + + it 'changes the status of sent? to true when a message is sent' do + expect { subject.send_sms(ENV['MY_PHONE']) }.to change(subject, :sent?).to true + end + + it 'confirms a message has been sent' do + expect(subject.sms_sent_confirmation).to eq('A confirmation message has been sent to the number you provided') + end +end