diff --git a/.rdbgrc.breakpoints b/.rdbgrc.breakpoints new file mode 100644 index 00000000..e69de29b diff --git a/app.rb b/app.rb new file mode 100644 index 00000000..64c8a228 --- /dev/null +++ b/app.rb @@ -0,0 +1,98 @@ +require_relative 'lib/database_connection' +require_relative './lib/item_repository' +require_relative './lib/order_repository' + +# Could refactor run method so the logic for each step is pulled out into separate methods + +class Application + def initialize(database_name, io, item_repository, order_repository) + DatabaseConnection.connect(database_name) + @io = io + @item_repository = item_repository + @order_repository = order_repository + end + + def run + action_list = "1 = list all shop items\n2 = create a new item\n3 = list all orders\n4 = create a new order\n5 = quit program\n\n" + action = nil + @io.puts "\nWelcome to the shop management program!\n" + + until action == "exit" + @io.puts "\nWhat do you want to do?\n#{action_list}" + action = @io.gets.chomp + + case action + when "1" + items = @item_repository.all + items.each do |item| + @io.puts "\n#{item.id}. #{item.name} - unit price: #{item.price} - quantity: #{item.quantity}" + end + + when "2" + item = Item.new + @io.puts "What is the name of the item?" + item.name = @io.gets.chomp + @io.puts "What is the price of the item?" + item.price = @io.gets.chomp + @io.puts "How many of the item will be in stock?" + item.quantity = @io.gets.chomp + + @item_repository.create(item) + + @io.puts "#{item.name} added to the database." + + when "3" + orders = @order_repository.all + + orders.each do |order| + @io.puts "\n#{order.id}. customer name: #{order.customer} - order date: #{order.date}\nOrder contents:\n" + items = @item_repository.find_by_order(order.id) + items.each do |item| + @io.puts "#{item.name} - unit price: #{item.price}\n" + end + end + + when "4" + order = Order.new + @io.puts "What is the customer's name?" + order.customer = @io.gets.chomp + @io.puts "What date was the order made?" + order.date = @io.gets.chomp + @order_repository.create(order) + last_id = @order_repository.all.last.id + @created_order = @order_repository.find(last_id) + @io.puts "How many items do you want to add to the order?" + number = @io.gets.chomp.to_i + number.times() do + @io.puts "What is the id of the item you want to add?" + item_id = @io.gets.chomp + item = @item_repository.find(item_id) + if item.quantity == '0' + @io.puts "That item is out of stock. Item could not be added. Please start again." + @order_repository.delete(@created_order.id) + else + @created_order.add_item(item) + item.quantity = item.quantity.to_i - 1 + @item_repository.update(item) + @io.puts "#{item.name} added to order." + end + end + + when "5" + break + + else + @io.puts "Invalid input. Please input a number from 1 to 5." + end + end + end +end + +if __FILE__ == $0 + app = Application.new( + 'shop_manager', + Kernel, + ItemRepository.new, + OrderRepository.new) + app.run +end diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 00000000..af6510ad --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,28 @@ +# file: lib/database_connection.rb + +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..572b9c19 --- /dev/null +++ b/lib/item.rb @@ -0,0 +1,3 @@ +class Item + attr_accessor :id, :name, :price, :quantity +end diff --git a/lib/item_repository.rb b/lib/item_repository.rb new file mode 100644 index 00000000..00f27b43 --- /dev/null +++ b/lib/item_repository.rb @@ -0,0 +1,85 @@ +require_relative './item' + +class ItemRepository + def all + sql = 'SELECT id, name, price, quantity FROM items;' + result_set = DatabaseConnection.exec_params(sql, []) + + items = [] + + result_set.each do |record| + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.price = record['price'] + item.quantity = record['quantity'] + + items << item + end + + items + end + + def find(id) + sql = 'SELECT id, name, price, quantity FROM items WHERE id = $1;' + params = [id] + + result = DatabaseConnection.exec_params(sql, params) + + result.each do |record| + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.price = record['price'] + item.quantity = record['quantity'] + + return item + end + end + + def find_by_order(order_id) + sql = 'SELECT items.id, items.name, items.price, items.quantity + FROM items + JOIN items_orders ON items_orders.item_id = items.id + JOIN orders ON items_orders.order_id = orders.id + WHERE orders.id = $1;' + params = [order_id] + + result_set = DatabaseConnection.exec_params(sql, params) + + items = [] + + result_set.each do |record| + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.price = record['price'] + item.quantity = record['quantity'] + + items << item + end + + items + end + + def create(item) + sql = 'INSERT INTO items (name, price, quantity) VALUES ($1, $2, $3);' + params = [item.name, item.price, item.quantity] + + DatabaseConnection.exec_params(sql, params) + end + + def update(item) # need to update quantity when an item is added to an order + sql = 'UPDATE items SET quantity = $1 WHERE id = $2;' + params = [item.quantity, item.id] + + DatabaseConnection.exec_params(sql, params) + end + + def delete(id) # when quantity reaches zero, need to delete item from stock + sql = 'DELETE FROM items WHERE id = $1;' + params = [id] + + DatabaseConnection.exec_params(sql, params) + end +end diff --git a/lib/order.rb b/lib/order.rb new file mode 100644 index 00000000..214ac5b4 --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,13 @@ +class Order + attr_accessor :id, :customer, :date + + def add_item(item) + order_id = self.id + item_id = item.id + + join_sql = 'INSERT INTO items_orders (item_id, order_id) VALUES ($1, $2);' + join_params = [item_id, order_id] + + DatabaseConnection.exec_params(join_sql, join_params) + end +end diff --git a/lib/order_repository.rb b/lib/order_repository.rb new file mode 100644 index 00000000..9341f039 --- /dev/null +++ b/lib/order_repository.rb @@ -0,0 +1,51 @@ +require_relative './order' + +class OrderRepository + def all + sql = 'SELECT id, customer, date FROM orders;' + result_set = DatabaseConnection.exec_params(sql, []) + + orders = [] + + result_set.each do |record| + order = Order.new + order.id = record['id'] + order.customer = record['customer'] + order.date = record['date'] + + orders << order + end + + orders + end + + def find(id) + sql = 'SELECT id, customer, date FROM orders WHERE id = $1;' + params = [id] + + result = DatabaseConnection.exec_params(sql, params) + + result.each do |record| + order = Order.new + order.id = record['id'] + order.customer = record['customer'] + order.date = record['date'] + + return order + end + end + + def create(order) + sql = 'INSERT INTO orders (customer, date) VALUES ($1, $2);' + params = [order.customer, order.date] + + DatabaseConnection.exec_params(sql, params) + end + + def delete(id) + sql = 'DELETE FROM orders WHERE id = $1;' + params = [id] + + DatabaseConnection.exec_params(sql, params) + end +end diff --git a/recipes/items_orders.sql b/recipes/items_orders.sql new file mode 100644 index 00000000..1f9d051c --- /dev/null +++ b/recipes/items_orders.sql @@ -0,0 +1,22 @@ +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + price int, + quantity int +); + +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer 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) +); diff --git a/recipes/repo_items.md b/recipes/repo_items.md new file mode 100644 index 00000000..12eefb36 --- /dev/null +++ b/recipes/repo_items.md @@ -0,0 +1,272 @@ +# Items Model and Repository Classes Design Recipe + +_Copy this recipe 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 recipe to design and create the SQL schema for your table](./single_table_design_recipe_template.md). + +*In this template, we'll use an example table `items`* + +``` +# EXAMPLE + +Table: items + +Columns: +id | name | price | quantity +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +```sql +-- EXAMPLE +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE items RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO items (name, cohort_name) VALUES ('David', 'April 2022'); +INSERT INTO items (name, cohort_name) VALUES ('Anna', 'May 2022'); +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 your_database_name < seeds_{table_name}.sql +``` + +## 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, :price, :quantity +end + +# The keyword attr_accessor is a special Ruby feature +# which allows us to set and get attributes on an object, +# here's an example: +# +# item = item.new +# item.name = 'Jo' +# item.name +``` + +*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 id, name, price, quantity FROM items; + + # Returns an array of item objects. + end + + # Gets a single record by its ID + # One argument: the id (number) + def find(id) + # Executes the SQL query: + # SELECT id, name, price, quantity FROM items WHERE id = $1; + + # Returns a single item object. + end + + # Gets all the items in a specific order + def find_by_order(order_id) # params for order_id will be $1 + # SELECT items.name, items.price FROM items JOIN items_orders ON items_orders.item_id = items.id + # JOIN orders ON items_orders.order_id = orders.id WHERE orders.id = $1; + end + + # Add more methods below for each operation you'd like to implement. + + def create(item) + # INSERT INTO items (name, price, quantity) VALUES ($1, $2, $3); + end + + def update(item) # need to update quantity when an item is added to an order + # UPDATE items SET quantity = $1 WHERE id = $2; + end + + def delete(id) # when quantity reaches zero, need to delete item from stock + # DELETE FROM items WHERE id = $1; + 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 # => 6 + +items[0].id # => 1 +items[0].name # => 'milk' +items[0].price # => '2' +items[0].quantity # => '50' + +# 2 +# Get a single item +repo = ItemRepository.new + +item = repo.find(1) + +item.id # => '1' +item.name # => 'milk' +item.price # => '2' +item.quantity # => '50' + +# 3 +# Gets all the items in a specific order +repo = ItemRepository.new + +items = repo.find_by_order(3) + +items.length # => 3 +items[0].id # => '2' +items[0].name # => 'bread' +items[0].price # => '3' +items[0].quantity # => '30' + +# 4 +# Adds a new item to the database +new_item = Item.new +new_item.name = 'cereal' +new_item.price = 5 +new_item.quantity = 70 + +repo = ItemRepository.new +repo.create(new_item) + +items = repo.all +last_item = items.last + +last_item.id # => '6' +last_item.name # => 'cereal' +last_item.price # => '5' +last_item.quantity # => '70' + +# 5 +# Updates the quantity of an item in the database # do logic in app.rb +repo = ItemRepository.new +item = repo.find(1) +item.quantity = 49 +repo.update(item) +updated_item = repo.find(1) + +updated_item.quantity # => '49' + +# 6 +# Deletes an item from the database (when it runs out - for integration spec?) +repo = ItemRepository.new +repo.delete(6) +items = repo.all + +items.length # => 5 +items.last.id # => '5' +items.last.name # => 'broccoli' +items.last.price # => '1' +items.last.quantity # => '45' +``` + +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_items_table + seed_sql = File.read('spec/seeds_items.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'items' }) + connection.exec(seed_sql) +end + +describe itemRepository do + before(:each) do + reset_items_table + 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/repo_orders.md b/recipes/repo_orders.md new file mode 100644 index 00000000..4ae1be78 --- /dev/null +++ b/recipes/repo_orders.md @@ -0,0 +1,233 @@ +# Orders Model and Repository Classes Design Recipe + +_Copy this recipe 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 recipe to design and create the SQL schema for your table](./single_table_design_recipe_template.md). + +*In this template, we'll use an example table `orders`* + +``` +# EXAMPLE + +Table: orders + +Columns: +id | customer | date +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +```sql +-- EXAMPLE +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE orders RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO orders (name, cohort_name) VALUES ('David', 'April 2022'); +INSERT INTO orders (name, cohort_name) VALUES ('Anna', 'May 2022'); +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 your_database_name < seeds_{table_name}.sql +``` + +## 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, :date, :items + @items = [] # this will be an array of Item objects +end + +# The keyword attr_accessor is a special Ruby feature +# which allows us to set and get attributes on an object, +# here's an example: +# +# order = order.new +# order.name = 'Jo' +# order.name +``` + +*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, customer, date FROM orders; + + # Returns an array of order objects. + end + + # Gets a single record by its ID + # One argument: the id (number) + def find(id) + # Executes the SQL query: + # SELECT id, customer, date FROM orders WHERE id = $1; + + # Returns a single order object. + end + + # Add more methods below for each operation you'd like to implement. + + def create(order) # needs to assign items to the order using the join table + # INSERT INTO orders (customer, date, items) VALUES ($1, $2, $3); # items will be an array of Item objects - with quantity one less + # INSERT INTO items_orders (item_id, order_id) VALUES ($4, $5); # will need to loop through item array and do a new insert for every item + + # when an item is added to the order, need to change the quantity - do this in app.rb? + # item_repo = ItemRepository.new + # item_repo.update(order.item) # loop through item array + end + + # def update(order) + # end + + # def delete(order) + # 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[0].id # => 1 +orders[0].customer # => 'Quack Overflow' +orders[0].date # => '04/01/23' + +# 2 +# Get a single order +repo = OrderRepository.new +order = repo.find(1) + +order.id # => '1' +order.customer # => 'Quack Overflow' +order.date # => '2023-04-01' + +# 3 +# Adds a new order to the database +new_order = Order.new +item_repo = ItemRepository.new +items = item_repo.all +order_repo = OrderRepository.new + +new_order.customer = 'Big Bird' +new_order.date = '03/29/23' +new_order.items.push(items[3], items[2]) + +order_repo.create(new_order) +orders = order_repo.all +last_order = orders.last + +last_order.id # => '4' +last_order.customer # => 'Big Bird' +last_order.date # => '2023-03-29' +last_order.items # => [items[3], items[2]] +``` + +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_orders_table + seed_sql = File.read('spec/seeds_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'orders' }) + connection.exec(seed_sql) +end + +describe orderRepository do + before(:each) do + reset_orders_table + 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/table_schema.md b/recipes/table_schema.md new file mode 100644 index 00000000..196dc92e --- /dev/null +++ b/recipes/table_schema.md @@ -0,0 +1,154 @@ +# 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 + +``` +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: + +items, name, price, quantity, orders, customer, date +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| items | name, price, quantity +| orders | customer, date + +1. Name of the first table (always plural): `items` + + Column names: `name`, `price`, `quantity`, `order_id` + +2. Name of the second table (always plural): `orders` + + Column names: `customer`, `date`, `item_id` + +## 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: items +id: SERIAL +name: text +price: int +quantity: int + +Table: orders +id: SERIAL +customer: 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 +2. Can one [TABLE TWO] have many [TABLE ONE]? Yes + +``` +# EXAMPLE + +1. Can one item have many orders? YES +2. Can one order have many items? 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 `table1_table2`. + +``` +# 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 + +-- Replace the table name, columm names and types. + +-- Create the first table. +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + price int, + quantity int +); + +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer 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 shop_manager < items_orders.sql +``` + diff --git a/spec/app_spec.rb b/spec/app_spec.rb new file mode 100644 index 00000000..6dcbec6b --- /dev/null +++ b/spec/app_spec.rb @@ -0,0 +1,173 @@ +require_relative '../app' + +# could double ItemRepository and OrderRepository as well + +def reset_items_table + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) +end + +RSpec.describe Application do + before(:each) do + reset_items_table + end + + let(:intro) do + "\nWelcome to the shop management program!\n" + end + + let(:action_list) do + "\nWhat do you want to do?\n1 = list all shop items\n2 = create a new item\n3 = list all orders\n4 = create a new order\n5 = quit program\n\n" + end + + it "lists all shop items" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("1").ordered + expect(@io).to receive(:puts).with("\n1. milk - unit price: 2 - quantity: 50").ordered + expect(@io).to receive(:puts).with("\n2. bread - unit price: 3 - quantity: 30").ordered + expect(@io).to receive(:puts).with("\n3. cake - unit price: 9 - quantity: 10").ordered + expect(@io).to receive(:puts).with("\n4. bananas - unit price: 4 - quantity: 100").ordered + expect(@io).to receive(:puts).with("\n5. broccoli - unit price: 1 - quantity: 45").ordered + expect(@io).to receive(:puts).with("\n6. rare item - unit price: 1000 - quantity: 0").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + app = Application.new('shop_manager_test', @io, ItemRepository.new, OrderRepository.new) + app.run + end + + it "creates a new item" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("2").ordered + expect(@io).to receive(:puts).with("What is the name of the item?").ordered + expect(@io).to receive(:gets).and_return("cereal").ordered + expect(@io).to receive(:puts).with("What is the price of the item?").ordered + expect(@io).to receive(:gets).and_return("5").ordered + expect(@io).to receive(:puts).with("How many of the item will be in stock?").ordered + expect(@io).to receive(:gets).and_return("70").ordered + expect(@io).to receive(:puts).with("cereal added to the database.").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + app = Application.new('shop_manager_test', @io, ItemRepository.new, OrderRepository.new) + app.run + end + + it "lists all orders with their associated items" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("3").ordered + expect(@io).to receive(:puts).with("\n1. customer name: Quack Overflow - order date: 2023-04-01\nOrder contents:\n").ordered + expect(@io).to receive(:puts).with("milk - unit price: 2\n").ordered + expect(@io).to receive(:puts).with("bread - unit price: 3\n").ordered + expect(@io).to receive(:puts).with("cake - unit price: 9\n").ordered + expect(@io).to receive(:puts).with("bananas - unit price: 4\n").ordered + expect(@io).to receive(:puts).with("broccoli - unit price: 1\n").ordered + expect(@io).to receive(:puts).with("\n2. customer name: Scrooge McDuck - order date: 2023-03-31\nOrder contents:\n").ordered + expect(@io).to receive(:puts).with("broccoli - unit price: 1\n").ordered + expect(@io).to receive(:puts).with("\n3. customer name: Silly Goose - order date: 2023-03-30\nOrder contents:\n").ordered + expect(@io).to receive(:puts).with("bread - unit price: 3\n").ordered + expect(@io).to receive(:puts).with("bananas - unit price: 4\n").ordered + expect(@io).to receive(:puts).with("broccoli - unit price: 1\n").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + app = Application.new('shop_manager_test', @io, ItemRepository.new, OrderRepository.new) + app.run + end + + it "creates a new order" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("4").ordered + expect(@io).to receive(:puts).with("What is the customer's name?").ordered + expect(@io).to receive(:gets).and_return("Big Bird").ordered + expect(@io).to receive(:puts).with("What date was the order made?").ordered + expect(@io).to receive(:gets).and_return("2023-01-01").ordered + expect(@io).to receive(:puts).with("How many items do you want to add to the order?").ordered + expect(@io).to receive(:gets).and_return("1").ordered + expect(@io).to receive(:puts).with("What is the id of the item you want to add?").ordered + expect(@io).to receive(:gets).and_return("1").ordered + expect(@io).to receive(:puts).with("milk added to order.").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + app = Application.new('shop_manager_test', @io, ItemRepository.new, OrderRepository.new) + app.run + end + + it "updates an item's stock when it is added to an order" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("4").ordered + expect(@io).to receive(:puts).with("What is the customer's name?").ordered + expect(@io).to receive(:gets).and_return("Big Bird").ordered + expect(@io).to receive(:puts).with("What date was the order made?").ordered + expect(@io).to receive(:gets).and_return("2023-01-01").ordered + expect(@io).to receive(:puts).with("How many items do you want to add to the order?").ordered + expect(@io).to receive(:gets).and_return("1").ordered + expect(@io).to receive(:puts).with("What is the id of the item you want to add?").ordered + expect(@io).to receive(:gets).and_return("1").ordered + expect(@io).to receive(:puts).with("milk added to order.").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + @item_repository = ItemRepository.new + app = Application.new('shop_manager_test', @io, @item_repository, OrderRepository.new) + app.run + + expect(@item_repository.find(1).quantity).to eq "49" + end + + it "cancels order creation if user attempts to add an out of stock item" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("4").ordered + expect(@io).to receive(:puts).with("What is the customer's name?").ordered + expect(@io).to receive(:gets).and_return("Big Bird").ordered + expect(@io).to receive(:puts).with("What date was the order made?").ordered + expect(@io).to receive(:gets).and_return("2023-01-01").ordered + expect(@io).to receive(:puts).with("How many items do you want to add to the order?").ordered + expect(@io).to receive(:gets).and_return("1").ordered + expect(@io).to receive(:puts).with("What is the id of the item you want to add?").ordered + expect(@io).to receive(:gets).and_return("6").ordered + expect(@io).to receive(:puts).with("That item is out of stock. Item could not be added. Please start again.").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + @order_repository = OrderRepository.new + app = Application.new('shop_manager_test', @io, ItemRepository.new, @order_repository) + app.run + + expect(@order_repository.all.length).to eq 3 + end + + it "prompts user to input a valid input at the action list" do + @io = double :io + + expect(@io).to receive(:puts).with(intro).ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("blah").ordered + expect(@io).to receive(:puts).with("Invalid input. Please input a number from 1 to 5.").ordered + expect(@io).to receive(:puts).with(action_list).ordered + expect(@io).to receive(:gets).and_return("5").ordered + + app = Application.new('shop_manager_test', @io, ItemRepository.new, OrderRepository.new) + app.run + end +end diff --git a/spec/item_repository_spec.rb b/spec/item_repository_spec.rb new file mode 100644 index 00000000..da533871 --- /dev/null +++ b/spec/item_repository_spec.rb @@ -0,0 +1,84 @@ +require 'item_repository' + +def reset_items_table + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) +end + +RSpec.describe ItemRepository do + before(:each) do + reset_items_table + end + + it "gets all items" do + repo = ItemRepository.new + + items = repo.all + + expect(items.length).to eq 6 + expect(items[0].id).to eq '1' + expect(items[0].name).to eq 'milk' + expect(items[0].price).to eq '2' + expect(items[0].quantity).to eq '50' + end + + it "gets a single item" do + repo = ItemRepository.new + item = repo.find(1) + + expect(item.id).to eq '1' + expect(item.name).to eq 'milk' + expect(item.price).to eq '2' + expect(item.quantity).to eq '50' + end + + it "gets all the items in a specific order" do + repo = ItemRepository.new + items = repo.find_by_order(3) + + expect(items.length).to eq 3 + expect(items[0].name).to eq 'bread' + expect(items[0].price).to eq '3' + end + + it "adds a new item to the database" do + new_item = Item.new + new_item.name = 'cereal' + new_item.price = 5 + new_item.quantity = 70 + + repo = ItemRepository.new + repo.create(new_item) + + items = repo.all + last_item = items.last + + expect(last_item.id).to eq '7' + expect(last_item.name).to eq 'cereal' + expect(last_item.price).to eq '5' + expect(last_item.quantity).to eq '70' + end + + it "updates the quantity of an item" do + repo = ItemRepository.new + item = repo.find(1) + item.quantity = 49 + repo.update(item) + updated_item = repo.find(1) + + expect(updated_item.quantity).to eq '49' + end + + it "deletes an item from the database" do + repo = ItemRepository.new + repo.delete(6) + items = repo.all + + expect(items.length).to eq 5 + expect(items.last.id).to eq '5' + expect(items.last.name).to eq 'broccoli' + expect(items.last.price).to eq '1' + expect(items.last.quantity).to eq '45' + end +end diff --git a/spec/order_repository_spec.rb b/spec/order_repository_spec.rb new file mode 100644 index 00000000..e192fcaa --- /dev/null +++ b/spec/order_repository_spec.rb @@ -0,0 +1,81 @@ +require 'order_repository' +require 'item_repository' # could double ItemRepository here instead of requiring it + +def reset_orders_table + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) +end + +RSpec.describe OrderRepository do + before(:each) do + reset_orders_table + end + + it "gets all orders" do + repo = OrderRepository.new + orders = repo.all + + expect(orders.length).to eq 3 + expect(orders[0].id).to eq '1' + expect(orders[0].customer).to eq 'Quack Overflow' + expect(orders[0].date).to eq '2023-04-01' + end + + it "gets a single order" do + repo = OrderRepository.new + order = repo.find(1) + + expect(order.id).to eq '1' + expect(order.customer).to eq 'Quack Overflow' + expect(order.date).to eq '2023-04-01' + end + + it "adds a new order to the database" do + new_order = Order.new + item_repo = ItemRepository.new + items = item_repo.all + order_repo = OrderRepository.new + + new_order.customer = 'Big Bird' + new_order.date = '03/29/23' + + order_repo.create(new_order) + orders = order_repo.all + last_order = orders.last + + expect(last_order.id).to eq '4' + expect(last_order.customer).to eq 'Big Bird' + expect(last_order.date).to eq '2023-03-29' + end + + it "adds items to an order" do + new_order = Order.new + item_repo = ItemRepository.new + items = item_repo.all + order_repo = OrderRepository.new + + new_order.customer = 'Big Bird' + new_order.date = '03/29/23' + + order_repo.create(new_order) + order = order_repo.find(4) + order.add_item(items[3]) + order.add_item(items[2]) + orders = order_repo.all + last_order = orders.last + expect(item_repo.find_by_order(4)[0].name).to eq 'cake' + expect(item_repo.find_by_order(4)[0].price).to eq '9' + end + + it "deletes an order by id" do + order_repo = OrderRepository.new + order_repo.delete(1) + orders = order_repo.all + first_order = orders.first + + expect(first_order.id).to eq '2' + expect(first_order.customer).to eq 'Scrooge McDuck' + expect(first_order.date).to eq '2023-03-31' + end +end diff --git a/spec/seeds.sql b/spec/seeds.sql new file mode 100644 index 00000000..3519fed9 --- /dev/null +++ b/spec/seeds.sql @@ -0,0 +1,25 @@ +TRUNCATE TABLE items, orders, items_orders RESTART IDENTITY; + +INSERT INTO items (name, price, quantity) VALUES +('milk', 2, 50), +('bread', 3, 30), +('cake', 9, 10), +('bananas', 4, 100), +('broccoli', 1, 45), +('rare item', 1000, 0); + +INSERT INTO orders (customer, date) VALUES +('Quack Overflow', '04/01/23'), +('Scrooge McDuck', '03/31/23'), +('Silly Goose', '03/30/23'); + +INSERT INTO items_orders (item_id, order_id) VALUES +(1, 1), +(2, 1), +(2, 3), +(3, 1), +(4, 1), +(4, 3), +(5, 1), +(5, 2), +(5, 3); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d8..96cdd153 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,10 +4,18 @@ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter + SimpleCov::Formatter::HTMLFormatter ]) SimpleCov.start +# file: spec/spec_helper.rb + +require 'database_connection' + +# Make sure this connects to your test database +# (its name should end with '_test') +DatabaseConnection.connect('shop_manager_test') + RSpec.configure do |config| config.after(:suite) do puts