diff --git a/README.md b/README.md index 242f94be..65556f4a 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,32 @@ Shop Manager Project ================= -* 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 next Monday morning +Welcome to the Shop Manager! -Challenge: -------- - -Please start by [forking this repo](https://github.com/makersacademy/shop-manager-challenge/fork), then clone your fork to your local machine. Work into that directory. - -We are going to write a small terminal program allowing the user to manage a shop database containing some items and orders. - -User stories: -------- +You can run the program using: ``` -As a shop manager -So I can know which items I have in stock -I want to keep a list of my shop items with their name and unit price. - -As a shop manager -So I can know which items I have in stock -I want to know which quantity (a number) I have for each item. - -As a shop manager -So I can manage items -I want to be able to create a new item. - -As a shop manager -So I can know which orders were made -I want to keep a list of orders with their customer name. +ruby app.rb +``` -As a shop manager -So I can know which orders were made -I want to assign each order to their corresponding item. +The program will continuously ask for actions to perform on the database until you select option 7 to exit the program -As a shop manager -So I can know which orders were made -I want to know on which date an order was placed. +### Database -As a shop manager -So I can manage orders -I want to be able to create a new order. +You will need to have a database running locally named 'items_orders'. +This can be achieved by running the following in the terminal ``` - -Here's an example of the terminal output your program should generate (yours might be slightly different — that's totally OK): - +createdb items_orders ``` -Welcome to the shop management program! - -What do you want to do? - 1 = list all shop items - 2 = create a new item - 3 = list all orders - 4 = create a new order - -1 [enter] - -Here's a list of all shop items: - - #1 Super Shark Vacuum Cleaner - Unit price: 99 - Quantity: 30 - #2 Makerspresso Coffee Machine - Unit price: 69 - Quantity: 15 - (...) +To initialise the tables run the following: ``` - -Technical Approach: ------ - -In this unit, you integrated a database by using the `PG` gem, and test-driving and building Repository classes. You can continue to use this approach when building this challenge. - -[You'll also need to mock IO](https://github.com/makersacademy/golden-square/blob/main/mocking_bites/05_unit_testing_terminal_io_bite.md) in your integration or unit tests, since the program will ask for user input. - -Notes on test coverage ----------------------- - -Please ensure you have the following **AT THE TOP** of your spec_helper.rb in order to have test coverage stats generated -on your pull request: - -```ruby -require 'simplecov' -require 'simplecov-console' - -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::Console, - # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter -]) -SimpleCov.start +psql -h 127.0.0.1 items_orders < spec/seeds_create.sql +``` +If you want to import some existing data into this table you can use the provided seed by entering the following into the terminal: +``` +psql -h 127.0.0.1 items_orders < spec/seeds.sql ``` +>hint: this will also refresh the table with the starter data if you wish to restart after adding data! +### WARNING -You can see your test coverage when you run your tests. If you want this in a graphical form, uncomment the `HTMLFormatter` line and see what happens! +Incorrect data entry wasn't in the scope of the program and can cause errors - **DOUBLE CHECK YOUR DATA ENTRY** diff --git a/app.rb b/app.rb new file mode 100644 index 00000000..2ed9c118 --- /dev/null +++ b/app.rb @@ -0,0 +1,9 @@ +Dir["./lib/*.rb"].each {|file| require file} + +app = Application.new( + 'items_orders', + Kernel, + ItemRepository.new, + OrderRepository.new +) +app.run \ No newline at end of file diff --git a/lib/application.rb b/lib/application.rb new file mode 100644 index 00000000..cb203e80 --- /dev/null +++ b/lib/application.rb @@ -0,0 +1,133 @@ +class Application + + # The Application class initializer + # takes four arguments: + # * The database name to call `DatabaseConnection.connect` + # * the Kernel object as `io` (so we can mock the IO in our tests) + # * the ItemRepository object (or a double of it) + # * the OrderRepository object (or a double of it) + def initialize(database_name, io, item_repository, order_repository) + DatabaseConnection.connect(database_name) + @io = io + @item_repository = item_repository + @order_repository = order_repository + end + + # "Runs" the terminal application + def run + loop do + print_menu + selection = @io.gets.chomp + do_selection(selection) + end + end + + # Takes the user's selection and perfoms the appropriate action + def do_selection(selection) + case selection + when "1" + print_items + when "2" + print_items_by_order + when "3" + create_item + when "4" + print_orders + when "5" + print_orders_by_item + when "6" + create_order + when "7" + @io.exit + end + end + + # Prints the menu + def print_menu + @io.puts "Welcome to the shop!" + @io.puts "What would you like to do?" + @io.puts "1 - List all items" + @io.puts "2 - List all items attached to an order" + @io.puts "3 - Create a new item" + @io.puts "4 - List all orders" + @io.puts "5 - List all orders that contain a specific item" + @io.puts "6 - Create a new order" + @io.puts "7 - Exit" + end + + # Prints all the items + def print_items + @io.puts "All items:" + @item_repository.all.each do |item| + print_item(item) + end + end + + # Prints all the items attached to an order + def print_items_by_order + @io.puts "What order do you want to see the items for?" + order_id = @io.gets.chomp.to_i + @item_repository.find_by_order(order_id).each do |item| + print_item(item) + end + end + + # Creates an item + def create_item + item = Item.new + + @io.print "Name: " + item.name = @io.gets.chomp + + @io.print "Price: " + item.unit_price = @io.gets.chomp.to_f + + @io.print "Quantity: " + item.quantity = @io.gets.chomp.to_i + + @item_repository.create(item) + @io.puts("Item created!") + end + + # Prints all the orders + def print_orders + @io.puts "All orders:" + @order_repository.all.each do |order| + print_order(order) + end + end + + # Prints all orders that have an item + def print_orders_by_item + @io.puts "What item do you want to see the orders for?" + item_id = @io.gets.chomp.to_i + + @order_repository.find_by_item(item_id).each do |order| + print_order(order) + end + end + + # Creates a new item + def create_order + order = Order.new + + @io.print "Name: " + order.customer_name = @io.gets.chomp + + @io.print "Date: " + order.date = @io.gets.chomp + + @order_repository.create(order) + @io.puts "Order created!" + end + + # Prints a single item + def print_item(item) + @io.puts "#{item.name} - Price: £#{sprintf('%.2f',item.unit_price)} - Quantity: #{item.quantity}" + end + + # Prints a single order + def print_order(order) + @io.puts "#{order.customer_name} - #{order.date}" + end +end diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 00000000..470de979 --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,26 @@ +require 'pg' + +# This class is a thin "wrapper" around the +# PG library. We'll use it in our project to interact +# with the database using SQL. + +class DatabaseConnection + # This method connects to PostgreSQL using the + # PG gem. We connect to 127.0.0.1, and select + # the database name given in argument. + def self.connect(database_name) + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + # This method executes an SQL query + # on the database, providing some optional parameters + # (you will learn a bit later about when to provide these parameters). + def self.exec_params(query, params) + if @connection.nil? + raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\ + 'the database was never opened. Did you make sure to call first the method '\ + '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?' + end + @connection.exec_params(query, params) + end +end diff --git a/lib/item.rb b/lib/item.rb new file mode 100644 index 00000000..50cde0ca --- /dev/null +++ b/lib/item.rb @@ -0,0 +1,3 @@ +class Item + attr_accessor :id, :name, :unit_price, :quantity +end diff --git a/lib/item_repository.rb b/lib/item_repository.rb new file mode 100644 index 00000000..655bd66c --- /dev/null +++ b/lib/item_repository.rb @@ -0,0 +1,65 @@ +class ItemRepository + + # Selecting all records + def all + # Executes the SQL query: + sql = "SELECT * FROM items;" + + result_set = DatabaseConnection.exec_params(sql, []) + + return result_set_to_items(result_set) + end + + # Adding an item to the table + # item: Item - item to add to table + def create(item) + # Executes the SQL query: + sql = 'INSERT INTO items (name, unit_price, quantity) VALUES ($1,$2,$3)' + sql_params = [item.name,item.unit_price,item.quantity] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + # Find all the items attached to an order + # order_id: int - the id of the order to filter by + def find_by_order(order_id) + sql = + 'SELECT + items.id, + items.name, + items.unit_price, + items.quantity + FROM items + JOIN items_orders + ON items.id = items_orders.item_id + JOIN orders + ON items_orders.order_id = orders.id + WHERE order_id = $1;' + + sql_params = [order_id] + result_set = DatabaseConnection.exec_params(sql, sql_params) + + # Returns an array of item objects. + return result_set_to_items(result_set) + end + + # Converts an SQL result set into a list of items + # result_set: tuple - whats returned when querying a table + def result_set_to_items(result_set) + items = [] + + result_set.each do |record| + item = Item.new + item.id = record['id'].to_i + item.name = record['name'] + item.unit_price = record['unit_price'].to_f + item.quantity = record['quantity'].to_i + + items << item + end + + return items + end +end diff --git a/lib/order.rb b/lib/order.rb new file mode 100644 index 00000000..5cce4c2a --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,3 @@ +class Order + attr_accessor :id, :customer_name, :date +end diff --git a/lib/order_repository.rb b/lib/order_repository.rb new file mode 100644 index 00000000..6014c27b --- /dev/null +++ b/lib/order_repository.rb @@ -0,0 +1,58 @@ +class OrderRepository + + # Selecting all records + def all + sql = "SELECT * FROM orders;" + result_set = DatabaseConnection.exec_params(sql, []) + + return result_set_to_orders(result_set) + end + + # Adding an order to the table + # order: Order - order to add to table + def create(order) + sql = 'INSERT INTO orders (customer_name, date) VALUES ($1,$2)' + sql_params = [order.customer_name, order.date] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + # Find all the orders attached to an item + # item_id: int - the id of the item to filter by + def find_by_item(item_id) + sql = + 'SELECT + orders.id, + orders.customer_name, + orders.date + FROM items + JOIN items_orders + ON items.id = items_orders.item_id + JOIN orders + ON items_orders.order_id = orders.id + WHERE item_id = $1;' + sql_params = [item_id] + + result_set = DatabaseConnection.exec_params(sql, sql_params) + + return result_set_to_orders(result_set) + end + + # Converts an SQL result set into a list of orders + # result_set: tuple - whats returned when querying a table + def result_set_to_orders(result_set) + orders = [] + result_set.each do |record| + order = Order.new + order.id = record['id'].to_i + order.customer_name = record['customer_name'] + order.date = record['date'] + + orders << order + end + + return orders + end +end diff --git a/recipes/items_recipe.md b/recipes/items_recipe.md new file mode 100644 index 00000000..f6ea2cfc --- /dev/null +++ b/recipes/items_recipe.md @@ -0,0 +1,204 @@ +# {{items}} Model and Repository Classes Design item + +_Copy this item template to design and implement Model and Repository classes for a database table._ + +## 1. Design and create the Table + +If the table is already created in the database, you can skip this step. + +Otherwise, [follow this item to design and create the SQL schema for your table](./single_table_design_item_template.md). + +*In this template, we'll use an example table `items`* + +``` +# EXAMPLE + +Table: items + +Columns: +id | name | unit_price | quantity +``` +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# EXAMPLE +# Table name: items + +# Model class +# (in lib/item.rb) +class Item +end + +# Repository class +# (in lib/item_repository.rb) +class ItemRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +```ruby +# EXAMPLE +# Table name: items + +# Model class +# (in lib/item.rb) + +class Item + + # Replace the attributes by your own columns. + attr_accessor :id, :name, :unit_price, :quantity +end +``` + +*You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed.* + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby +# EXAMPLE +# Table name: items + +# Repository class +# (in lib/item_repository.rb) + +class ItemRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT * FROM items; + + # Returns an array of item objects. + end + + # Adding an item to the table + # item: Item - item to add to table + def create(item) + # Executes the SQL query: + # INSERT INTO items (name, unit_price, quantity) VALUES ($1,$2,$3) + + # Returns nil + end + + # Find all the items attached to an order + # order_id: int - the id of the order to filter by + def find_by_order(order_id) + # Executes the SQL query: + # SELECT + # items.id AS "item_id", + # items.name, + # items.unit_price, + # items.quantity, + # orders.id AS "order_id", + # orders.customer_name, + # orders.date + # FROM items + # JOIN items_orders + # ON items.id = items_orders.item_id + # JOIN orders + # ON items_orders.order_id = orders.id + # WHERE order_id = 1; + + # Returns an array of item objects. + end +end +``` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby +# EXAMPLES + +# 1 +# Get all items + +repo = ItemRepository.new + +items = repo.all + +items.length # => 7 + +items.first.id # => 1 +items.first.name # => 'Pizza' +items.first.unit_price # => 9.99 +items.first.quantity # => 100 + +# 2 +# Create an item + +repo = ItemRepository.new + +item = Item.new +item.name = 'Doughnut' +item.unit_price = 3.99 +item.quantity = 250 + +repo.create(item) + +created_item = repo.all.last +created_item.id # => 8 +created_item.name # => 'Doughnut' +created_item.unit_price # => 3.99 +created_item.quantity # => 250 + +# 3 +# Find items attached to an order + +repo = ItemRepository.new + +items = repo.find_by_order(1) + +items.length # => 4 +items.first.id # => 1 +items.first.name # => 'Pizza' +items.first.unit_price # => 9.99 +items.first.quantity # => 100 + +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/item_repository_spec.rb + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +describe ItemRepository do + before(:each) do + reset_tables + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ + diff --git a/recipes/orders_recipe.md b/recipes/orders_recipe.md new file mode 100644 index 00000000..53e5f35d --- /dev/null +++ b/recipes/orders_recipe.md @@ -0,0 +1,201 @@ +# {{orders}} Model and Repository Classes Design order + +_Copy this order template to design and implement Model and Repository classes for a database table._ + +## 1. Design and create the Table + +If the table is already created in the database, you can skip this step. + +Otherwise, [follow this order to design and create the SQL schema for your table](./single_table_design_order_template.md). + +*In this template, we'll use an example table `orders`* + +``` +# EXAMPLE + +Table: orders + +Columns: +id | customer_name | date +``` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# EXAMPLE +# Table name: orders + +# Model class +# (in lib/order.rb) +class Order +end + +# Repository class +# (in lib/order_repository.rb) +class OrderRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +```ruby +# EXAMPLE +# Table name: orders + +# Model class +# (in lib/order.rb) + +class Order + + # Replace the attributes by your own columns. + attr_accessor :id, :customer_name, :date +end +``` + +*You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed.* + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby +# EXAMPLE +# Table name: orders + +# Repository class +# (in lib/order_repository.rb) + +class OrderRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, name, cohort_name FROM orders; + + # Returns an array of order objects. + end + + # Adding an order to the table + # order: Order - order to add to table + def create(order) + # Executes the SQL query: + # INSERT INTO orders (customer_name, date) VALUES ($1,$2) + + # Returns nil + end + + # Find all the orders attached to an item + # item_id: int - the id of the item to filter by + def find_by_item(item_id) + # Executes the SQL query: + # SELECT + # items.id AS "item_id", + # items.name, + # items.unit_price, + # items.quantity, + # orders.id AS "order_id", + # orders.customer_name, + # orders.date + # FROM items + # JOIN items_orders + # ON items.id = items_orders.item_id + # JOIN orders + # ON items_orders.order_id = orders.id + # WHERE item_id = $1; + + # Returns an array of order objects. + end +end +``` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby +# EXAMPLES + +# 1 +# Get all orders + +repo = OrderRepository.new + +orders = repo.all + +orders.length # => 3 + +orders.first.id # => 1 +orders.first.customer_name # => 'Sam' +orders.first.date # => '2023-03-31' + +# 2 +# Create an order + +repo = OrderRepository.new + +order = Order.new +order.customer_name = 'Laura' +order.date = '2023-04-01' + +repo.create(order) + +created_order = repo.all.last +created_order.id # => 4 +created_order.customer_name # => 'Laura' +created_order.date # => '2023-04-01' + +# 3 +# Find order attached to an order + +repo = OrderRepository.new + +orders = repo.find_by_item(1) + +orders.length # => 2 +orders.first.id # => 1 +orders.first.customer_name # => 'Sam' +orders.first.date # => '2023-03-31' + + +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/order_repository_spec.rb + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +describe OrderRepository do + before(:each) do + reset_tables + end + +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ + diff --git a/recipes/tables_recipe.md b/recipes/tables_recipe.md new file mode 100644 index 00000000..590a7e45 --- /dev/null +++ b/recipes/tables_recipe.md @@ -0,0 +1,149 @@ +# Two Tables (Many-to-Many) Design Recipe Template + +_Copy this recipe template to design and create two related database tables having a Many-to-Many relationship._ + +## 1. Extract nouns from the user stories or specification + +``` +# EXAMPLE USER STORIES: + +As a shop manager +So I can know which items I have in stock +I want to keep a list of my shop [items] with their [name] and [unit price]. + +As a shop manager +So I can know which items I have in stock +I want to know which [quantity] (a number) I have for each item. + +As a shop manager +So I can manage items +I want to be able to create a new item. + +As a shop manager +So I can know which orders were made +I want to keep a list of [orders] with their [customer name]. + +As a shop manager +So I can know which orders were made +I want to assign each order to their corresponding item. + +As a shop manager +So I can know which orders were made +I want to know on which [date] an order was placed. + +As a shop manager +So I can manage orders +I want to be able to create a new order. +``` + +``` +Nouns: + +item, item_name, item_unit_price, item_quantity, order, customer_name, date +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| item | name, unit_price, quanitity +| order | customer_name, date + +1. Name of the first table (always plural): `items` + + Column names: `name` + +2. Name of the second table (always plural): `orders` + + Column names: `emotion` + +## 3. Decide the column types. + +[Here's a full documentation of itemgreSQL data types](https://www.itemgresql.org/docs/current/datatype.html). + +Most of the time, you'll need either `text`, `int`, `bigint`, `numeric`, or `boolean`. If you're in doubt, do some research or ask your peers. + +Remember to **always** have the primary key `id` as a first column. Its type will always be `SERIAL`. + +``` +# EXAMPLE: + +Table: item +id: SERIAL +name: name, +unit_price: float, +quanitity: int + +Table: order +id: SERIAL +name: text +date: date +``` + +## 4. Design the Many-to-Many relationship + +Make sure you can answer YES to these two questions: + +1. Can one [TABLE ONE] have many [TABLE TWO]? (Yes/No) +2. Can one [TABLE TWO] have many [TABLE ONE]? (Yes/No) + +``` +# EXAMPLE + +1. Can one order have many items? YES +2. Can one item have many orders? YES +``` + +_If you would answer "No" to one of these questions, you'll probably have to implement a One-to-Many relationship, which is simpler. Use the relevant design recipe in that case._ + +## 5. Design the Join Table + +The join table usually contains two columns, which are two foreign keys, each one linking to a record in the two other tables. + +The naming convention is `items_orders`. + +``` +# EXAMPLE + +Join table for tables: items and orders +Join table name: items_orders +Columns: item_id, order_id +``` + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: items_orders.sql +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + unit_price float, + quantity int +); + +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date date +); + +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_item foreign key(item_id) references items(id) on delete cascade, + constraint fk_order foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); + +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 items_orders < items_orders.sql +``` \ No newline at end of file diff --git a/spec/application_spec.rb b/spec/application_spec.rb new file mode 100644 index 00000000..5f413cf5 --- /dev/null +++ b/spec/application_spec.rb @@ -0,0 +1,255 @@ +require 'item' +require 'item_repository' +require 'order' +require 'order_repository' +require 'application' + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +def create_app(io) + return Application.new( + 'items_orders_test', + io, + @item_repository, + @order_repository + ) +end + +RSpec.describe Application do + before(:each) do + reset_tables + @item_repository = ItemRepository.new + @order_repository = OrderRepository.new + end + + it "prints the menu" do + io = double :io + expect(io).to receive(:puts) + .with('Welcome to the shop!') + .ordered + expect(io).to receive(:puts) + .with('What would you like to do?') + .ordered + expect(io).to receive(:puts) + .with('1 - List all items') + .ordered + expect(io).to receive(:puts) + .with('2 - List all items attached to an order') + .ordered + expect(io).to receive(:puts) + .with('3 - Create a new item') + .ordered + expect(io).to receive(:puts) + .with('4 - List all orders') + .ordered + expect(io).to receive(:puts) + .with('5 - List all orders that contain a specific item') + .ordered + expect(io).to receive(:puts) + .with('6 - Create a new order') + .ordered + expect(io).to receive(:puts) + .with('7 - Exit') + .ordered + + app = create_app(io) + app.print_menu + end + + it "actions on user selection" do + io = double :io + app = create_app(io) + + expect(app).to receive(:print_items).ordered + app.do_selection('1') + + expect(app).to receive(:print_items_by_order).ordered + app.do_selection('2') + + expect(app).to receive(:create_item).ordered + app.do_selection('3') + + expect(app).to receive(:print_orders).ordered + app.do_selection('4') + + expect(app).to receive(:print_orders_by_item).ordered + app.do_selection('5') + + expect(app).to receive(:create_order).ordered + app.do_selection('6') + + expect(io).to receive(:exit).ordered + app.do_selection('7') + end + + it "prints all items" do + io = double :io + expect(io).to receive(:puts) + .with('All items:') + .ordered + expect(io).to receive(:puts) + .with("Pizza - Price: £9.99 - Quantity: 100") + .ordered + expect(io).to receive(:puts) + .with("Cake - Price: £4.50 - Quantity: 20") + .ordered + expect(io).to receive(:puts) + .with("Chips - Price: £2.50 - Quantity: 50") + .ordered + expect(io).to receive(:puts) + .with("Burger - Price: £8.49 - Quantity: 12") + .ordered + expect(io).to receive(:puts) + .with("Salad - Price: £0.99 - Quantity: 2") + .ordered + expect(io).to receive(:puts) + .with("Hotdog - Price: £12.50 - Quantity: 99") + .ordered + expect(io).to receive(:puts) + .with("Spagbol - Price: £19.99 - Quantity: 59") + .ordered + + app = create_app(io) + app.print_items + end + + it "prints items by order" do + io = double :io + expect(io).to receive(:puts) + .with("What order do you want to see the items for?") + .ordered + expect(io).to receive(:gets) + .and_return('1') + .ordered + expect(io).to receive(:puts) + .with("Pizza - Price: £9.99 - Quantity: 100") + .ordered + expect(io).to receive(:puts) + .with("Cake - Price: £4.50 - Quantity: 20") + .ordered + expect(io).to receive(:puts) + .with("Chips - Price: £2.50 - Quantity: 50") + .ordered + expect(io).to receive(:puts) + .with("Salad - Price: £0.99 - Quantity: 2") + .ordered + app = create_app(io) + app.print_items_by_order + end + + it "creates an item" do + io = double :io + expect(io).to receive(:print) + .with("Name: ") + .ordered + expect(io).to receive(:gets) + .and_return("Enchilada") + .ordered + expect(io).to receive(:print) + .with("Price: ") + .ordered + expect(io).to receive(:gets) + .and_return("7.99") + .ordered + expect(io).to receive(:print) + .with("Quantity: ") + .ordered + expect(io).to receive(:gets) + .and_return("60") + .ordered + expect(io).to receive(:puts) + .with("Item created!") + .ordered + + + app = create_app(io) + app.create_item + + new_item = @item_repository.all.last + expect(new_item.id).to eq 8 + expect(new_item.name).to eq "Enchilada" + expect(new_item.unit_price).to eq 7.99 + expect(new_item.quantity).to eq 60 + end + + it "prints all orders" do + io = double :io + expect(io).to receive(:puts) + .with('All orders:') + .ordered + expect(io).to receive(:puts) + .with('Sam - 2023-03-31') + .ordered + expect(io).to receive(:puts) + .with('Bob - 2023-02-28') + .ordered + expect(io).to receive(:puts) + .with('Jim - 2023-04-22') + .ordered + + app = create_app(io) + app.print_orders + end + + it "prints orders by item" do + io = double :io + expect(io).to receive(:puts) + .with("What item do you want to see the orders for?") + .ordered + expect(io).to receive(:gets) + .and_return('1') + .ordered + expect(io).to receive(:puts) + .with('Sam - 2023-03-31') + .ordered + expect(io).to receive(:puts) + .with('Jim - 2023-04-22') + .ordered + + app = create_app(io) + app.print_orders_by_item + end + + it "creates an order" do + io = double :io + expect(io).to receive(:print) + .with("Name: ") + .ordered + expect(io).to receive(:gets) + .and_return("Jeremy") + .ordered + expect(io).to receive(:print) + .with("Date: ") + .ordered + expect(io).to receive(:gets) + .and_return("2023-05-01") + .ordered + expect(io).to receive(:puts) + .with("Order created!") + .ordered + + app = create_app(io) + app.create_order + + created_order = @order_repository.all.last + expect(created_order.id).to eq 4 + expect(created_order.customer_name).to eq 'Jeremy' + expect(created_order.date).to eq '2023-05-01' + end + + xit "loops when the program is run" do + io = double :io + expect(io).to receive(:gets).and_return('1').ordered + expect(io).to receive(:gets).and_return('4').ordered + expect(io).to receive(:gets).and_return('7').ordered + expect(io).to receive(:exit).ordered + app = create_app(io) + expect(app).to receive(:print_items).ordered + expect(app).to receive(:print_orders).ordered + app.run + end +end diff --git a/spec/item_repository_spec.rb b/spec/item_repository_spec.rb new file mode 100644 index 00000000..93bd5c61 --- /dev/null +++ b/spec/item_repository_spec.rb @@ -0,0 +1,58 @@ +require 'item' +require 'item_repository' +require 'order' + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +describe ItemRepository do + before(:each) do + reset_tables + end + + it "finds all items" do + repo = ItemRepository.new + + items = repo.all + + expect(items.length).to eq 7 + + expect(items.first.id).to eq 1 + expect(items.first.name).to eq 'Pizza' + expect(items.first.unit_price).to eq 9.99 + expect(items.first.quantity).to eq 100 + end + + it "creates an item" do + repo = ItemRepository.new + + item = Item.new + item.name = 'Doughnut' + item.unit_price = 3.99 + item.quantity = 250 + + repo.create(item) + + created_item = repo.all.last + + expect(created_item.id).to eq 8 + expect(created_item.name).to eq 'Doughnut' + expect(created_item.unit_price).to eq 3.99 + expect(created_item.quantity).to eq 250 + end + + it "finds items associated to an order" do + repo = ItemRepository.new + + items = repo.find_by_order(1) + + expect(items.length).to eq 4 + expect(items.first.id).to eq 1 + expect(items.first.name).to eq 'Pizza' + expect(items.first.unit_price).to eq 9.99 + expect(items.first.quantity).to eq 100 + end +end diff --git a/spec/order_repository_spec.rb b/spec/order_repository_spec.rb new file mode 100644 index 00000000..3bef07bf --- /dev/null +++ b/spec/order_repository_spec.rb @@ -0,0 +1,53 @@ +require 'order' +require 'order_repository' +require 'item' + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items_orders_test' }) + connection.exec(seed_sql) +end + +describe OrderRepository do + before(:each) do + reset_tables + end + + it "finds all order" do + repo = OrderRepository.new + + orders = repo.all + + expect(orders.length).to eq 3 + + expect(orders.first.id).to eq 1 + expect(orders.first.customer_name).to eq 'Sam' + expect(orders.first.date).to eq '2023-03-31' + end + + it "creates an order" do + repo = OrderRepository.new + + order = Order.new + order.customer_name = 'Laura' + order.date = '2023-04-01' + + repo.create(order) + + created_order = repo.all.last + expect(created_order.id).to eq 4 + expect(created_order.customer_name).to eq 'Laura' + expect(created_order.date).to eq '2023-04-01' + end + + it "finds order associated with an item" do + repo = OrderRepository.new + + orders = repo.find_by_item(1) + + expect(orders.length).to eq 2 + expect(orders.first.id).to eq 1 + expect(orders.first.customer_name).to eq 'Sam' + expect(orders.first.date).to eq '2023-03-31' + end +end diff --git a/spec/seeds.sql b/spec/seeds.sql new file mode 100644 index 00000000..6c04d66d --- /dev/null +++ b/spec/seeds.sql @@ -0,0 +1,26 @@ +TRUNCATE TABLE items, orders, items_orders RESTART IDENTITY; + +INSERT INTO items + (name, unit_price, quantity) + VALUES + ('Pizza', 9.99, 100), + ('Cake', 4.50, 20), + ('Chips', 2.50, 50), + ('Burger', 8.49, 12), + ('Salad', 0.99, 2), + ('Hotdog', 12.50, 99), + ('Spagbol',19.99, 59); + +INSERT INTO orders + (customer_name, date) + VALUES + ('Sam', '2023-03-31'), + ('Bob', '2023-02-28'), + ('Jim', '2023-04-22'); + +INSERT INTO items_orders + (order_id, item_id) + VALUES + (1,1),(1,3),(1,5),(1,2), + (2,2),(2,7), + (3,1),(3,2),(3,3),(3,6),(3,7); \ No newline at end of file diff --git a/spec/seeds_create.sql b/spec/seeds_create.sql new file mode 100644 index 00000000..03682439 --- /dev/null +++ b/spec/seeds_create.sql @@ -0,0 +1,20 @@ +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + unit_price float, + quantity int +); + +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date date +); + +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_item foreign key(item_id) references items(id) on delete cascade, + constraint fk_order foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d8..d3bae5a2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,8 @@ require 'simplecov' require 'simplecov-console' +require 'database_connection' + +DatabaseConnection.connect('items_orders_test') SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console,