From d47c0aab05802a9ad876ddf81d114ecc3feb355a Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Thu, 5 Mar 2020 13:22:23 +0100 Subject: [PATCH 01/19] Create cart_to_react.md --- cart_to_react.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 cart_to_react.md diff --git a/cart_to_react.md b/cart_to_react.md new file mode 100644 index 00000000..d3e036e1 --- /dev/null +++ b/cart_to_react.md @@ -0,0 +1,44 @@ +## Adding cart functionality to React + +In this section we are going to add checkout functionality to our react application. +The prerequsitet for this functionality is that we have products on our page that are ready to sell. + +Start with making sure that you have the latest code pulled from github on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. + +As always we will work in a test driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. + + +Create a new feature file by typing +`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` + +Add the following tests to the file: + +```js +describe("User can add a product to their order", () => { + before(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: { message: "A product has been added to your order" } + }); + }); + + it("user gests a confirmation messsage when adding a a prodcut to order", () => { + cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API + cy.get("#product-1").within(() => { + cy.get("#button") + .contains("Add to order") + .click(); + }); + cy.wait(3000) + }); +}); + +``` From 346111ce58fc9e0a18a56798effd4b2d6e87673a Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Fri, 6 Mar 2020 12:37:54 +0100 Subject: [PATCH 02/19] Update cart_to_react.md --- cart_to_react.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/cart_to_react.md b/cart_to_react.md index d3e036e1..ba171c55 100644 --- a/cart_to_react.md +++ b/cart_to_react.md @@ -30,7 +30,7 @@ describe("User can add a product to their order", () => { }); }); - it("user gests a confirmation messsage when adding a a prodcut to order", () => { + it("user gests a confirmation messsage when adding a a producut to order", () => { cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API cy.get("#product-1").within(() => { cy.get("#button") @@ -40,5 +40,109 @@ describe("User can add a product to their order", () => { cy.wait(3000) }); }); +``` +So what are we excpecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? + +When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. + +In our case we already have a component that we use in order to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. + +Let's add the implementation code. + +We need to start with the first step which is to make sure that we have a button available to click on, let's add that. + +```jsx +
+ {`${item.name} ${item.description} ${item.price}`} + +
+``` +So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. + +Add the following code to the component: + +```js +addToOrder() { + debugger +} +``` + +So why are we adding a debugger here? Well first we need to make sure that the `addToOrder` button actually gets invoked. + +Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome broweser. + +If we can see that then its time for us to add the real functionality. +But first lets make sure that we are hitting the right button. +Go to your chrome console where the execution of the code has stopped and run the following command in the console: + +`event.target.parentElement` + +You should probably see something like this in your console: + +```html +
+ "Pizza" + "Crusty and fluffy" + "100 SEK" + +
+``` +If not then make sure that you are iterating over your products properly. + +So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it beacuse the values in the product are sent back to us in the form of an object. + +Add the dataset to the parent div: + +```js +
+ {`${item.name} ${item.description} ${item.price}`} + +
+``` + +Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. +you should see the same information as before, but the information about the product will be returend to us in the form of an object. +This is good new for us beacuse we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) +} ``` + +So what did we do here? First we made the method asyncronous beacuse we need to wait for a response from our API, we passed in the event as an argument to the function and we also added axios in order to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` + +Make sure that you add axios and also import it to the top of the file if you haven't already. + +The next thing we are going to focus on is to get the message to be displayed. + +Lets start with adding the message to the state: + +```js +state = { + productData: [], + message: {} + } +``` + +Next we need to display the message below the button. + +```js +[....] + + {parseInt(this.state.message.id) === item.id &&

{this.state.message.message}

} +``` + +Finally we need to set the new state in the `addToOrder` function + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +If we run our tests now everything should go green. From 2ba98a77439e34fdff366594e214c968fcf61046 Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Fri, 6 Mar 2020 19:24:10 +0100 Subject: [PATCH 03/19] Adds rails section --- cart_to_react.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/cart_to_react.md b/cart_to_react.md index ba171c55..9113e32c 100644 --- a/cart_to_react.md +++ b/cart_to_react.md @@ -146,3 +146,119 @@ async addToOrder(event) { ``` If we run our tests now everything should go green. + +Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. + +```bash +$ touch spec/requests/api/client_can_create_new_order_spec.rb +``` + +Add the following test to the spec file + +```ruby +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + before do + post '/api/orders', params: { product_id: product_1.id } + @order_id = JSON.parse(response.body)['order_id'] + end + + it 'responds with success message' do + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end +end +``` +Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. + +Run the test and make sure that you are getting the correct error messages. + +We will probably get an error message that states that we have an unitialized constant Api::OrdersController. + +We will fix this by generating a new orders controller. + +`$ rails generate controller Api::Orders create` + +Run the test again and you will get a new error message. + + +The error message probably has to do with our routes and that there are none. Lets fix that. + +```ruby +Rails.application.routes.draw do + namespace :api do + resources :products, only: [:index] + resources :orders, only: [:create, :update] + end +end +``` + +Our problem now is that we are not getting the right response. But this is expected. + +To fix this we need to add the order to the create action in our Orders controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + +The next step we need to take is to generate a Order model to have something to pull out from the database. + +`$ rails generate model order` + +We also need to associate the the order to the order items that we have, and the products. + +`$ rails generate model OrderItems order:references product:references` + +Go ahead and go through the files that are generated and make sure that there are no problems. + +We will now add specs to test the associations we created between the models. +In the newly generated `order_items_spec.rb` go ahead and add two specs. +You will need more than that but for this guide we will limit ourselves to these specs. + +```ruby +require 'rails_helper' + +RSpec.describe OrderItem, type: :model do + it { is_expected.to belong_to :order } + it { is_expected.to belong_to :product } +end +``` + +`order_spec.rb` + +```ruby +require 'rails_helper' + +RSpec.describe Order, type: :model do + it {is_expected.to have_many :order_items} +end +``` + +We need to make sure that the associations are present as well. + +`order_item.rb` + +```ruby +class OrderItem < ApplicationRecord + belongs_to :order + belongs_to :product +end +``` + +`order.rb` + +```ruby + +class Order < ApplicationRecord + has_many :order_items +end +``` + +Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. From c4ef9729a0b48a63bdebe006adf2c528ac92c2fe Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Wed, 11 Mar 2020 11:21:32 +0100 Subject: [PATCH 04/19] Update cart_to_react.md --- cart_to_react.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/cart_to_react.md b/cart_to_react.md index 9113e32c..56ae413f 100644 --- a/cart_to_react.md +++ b/cart_to_react.md @@ -262,3 +262,59 @@ end ``` Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. + + +### Returning to the client + +Let's go back tot the client and add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. + +We need to do a couple of things: + +Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. + +We will also add a `PUT` request to this block in order to update the order that has already been created. + +And at last we will add a test to check that we can add multiple products to an order. + +The final form of the feature will be the following: + +```js +describe('User can add a product to his/her order', () => { + + beforeEach(() => { + cy.server(); + cy.route({ + method: 'GET', + url: 'http://localhost:3000/api/products', + response: 'fixture:product_data.json' + }) + + cy.route({ + method: 'POST', + url: 'http://localhost:3000/api/orders', + response: { message: 'The product has been added to your order', order_id: 1 } + }) + + cy.route({ + method: 'PUT', + url: 'http://localhost:3000/api/orders/1', + response: { message: 'The product has been added to your order', order_id: 1 } + }) + cy.visit('http://localhost:3001') +}); + + it('user get a confirmation message when adding product to order', () => { + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +Make sure that you have added the product_id to the messages in the `beforeEach` function. From afe782d432bdf1f50244b648c04dc0d730c97fab Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Fri, 13 Mar 2020 08:17:06 +0100 Subject: [PATCH 05/19] Update cart_to_react.md --- cart_to_react.md | 149 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 138 insertions(+), 11 deletions(-) diff --git a/cart_to_react.md b/cart_to_react.md index 56ae413f..c187c968 100644 --- a/cart_to_react.md +++ b/cart_to_react.md @@ -157,17 +157,14 @@ Add the following test to the spec file ```ruby RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: 'Pizza') } - let!(:product_2) { create(:product, name: 'Kebab') } + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } - before do - post '/api/orders', params: { product_id: product_1.id } - @order_id = JSON.parse(response.body)['order_id'] - end + it 'responds with success message' do + post '/api/orders', params: {id: product_1.id } - it 'responds with success message' do - expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' - end + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end end ``` Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. @@ -255,15 +252,16 @@ end `order.rb` ```ruby - class Order < ApplicationRecord has_many :order_items end ``` -Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. +Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. +------ + ### Returning to the client Let's go back tot the client and add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. @@ -318,3 +316,132 @@ describe('User can add a product to his/her order', () => { ``` Make sure that you have added the product_id to the messages in the `beforeEach` function. + +Lets continue with the implementation code to get all of this to work. +add the `orderId` to the state +```js +state = { + productData: [], + message: {}, + orderId: '' + } +``` + +Now we need to update the `addToOrder` function with the new `orderId` state. + + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) +} +``` +The `order_id` is what is being returned to us from the backend. + +We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. + +```js + async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderId !== "") { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + } + this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) + } +``` + +So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. + +Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. + + +Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. + +We want to make sure that we can view the products that we have added to our order. + +First we want to make sure that there is a button where we can view the order. +However this button should only be visable when we have added products to our order. + +Refactor your code to look like this: + +```js + [....] + + it('user can add multiple products to order and view its content', () => { + cy.get('button').contains('View order').should('not.exist') // Add this line + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').should('exist') // Add this line + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +And now for the implementation, let's add the button in the `DisplayProducts` component + +```js +return ( + <> + {this.state.orderId !== '' && + } + {this.state.showOrder && +
    + {orderDetailsDisplay} +
+ } + {dataIndex} + +``` + + ### Update the order Backend + + From 3c5066c643c1f4995b2794b892af37123386b213 Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Fri, 13 Mar 2020 14:47:53 +0100 Subject: [PATCH 06/19] Create react-cart-updated.md --- react-cart-updated.md | 646 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 646 insertions(+) create mode 100644 react-cart-updated.md diff --git a/react-cart-updated.md b/react-cart-updated.md new file mode 100644 index 00000000..73426d1e --- /dev/null +++ b/react-cart-updated.md @@ -0,0 +1,646 @@ +## Adding cart functionality to React + +In this section we are going to add checkout functionality to our react application. +The prerequsitet for this functionality is that we have products on our page that are ready to sell. + +Start with making sure that you have the latest code pulled from github on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. + +As always we will work in a test driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. + + +Create a new feature file by typing +`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` + +Add the following tests to the file: + +```js +describe("User can add a product to their order", () => { + before(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: { message: "A product has been added to your order" } + }); + }); + + it("user gests a confirmation messsage when adding a a producut to order", () => { + cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API + cy.get("#product-1").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.wait(3000) + }); +}); +``` +So what are we excpecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? + +When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. + +In our case we already have a component that we use in order to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. + +Let's add the implementation code. + +We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are maping over the products. + +```jsx +
+ {`${item.name} ${item.description} ${item.price}`} + +
+``` +So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. + +Add the following code to the component: + +```js +addToOrder() { + debugger +} +``` + +So why are we adding a debugger here? Well first we need to make sure that the `addToOrder` button actually gets invoked. + +Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome broweser. + +If we can see that then its time for us to add the real functionality. +But first lets make sure that we are hitting the right button. +Go to your chrome console where the execution of the code has stopped and run the following command in the console: + +`event.target.parentElement` + +You should probably see something like this in your console: + +```html +
+ "Pizza" + "Crusty and fluffy" + "100 SEK" + +
+``` +If not then make sure that you are iterating over your products properly. + +So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it beacuse the values in the product are sent back to us in the form of an object. + +Add the dataset to the parent div: + +```js +
+ {`${item.name} ${item.description} ${item.price}`} + +
+``` + +Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. +you should see the same information as before, but the information about the product will be returend to us in the form of an object. + +This is good new for us beacuse we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) +} +``` + +So what did we do here? First we made the method asyncronous beacuse we need to wait for a response from our API, we passed in the event as an argument to the function and we also added axios in order to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` + +Make sure that you add axios and also import it to the top of the file if you haven't already. + +The next thing we are going to focus on is to get the message to be displayed. + +Lets start with adding the message to the state: + +```js +state = { + productData: [], + message: {} + } +``` + +Next we need to display the message below the button. + +```js +[....] + + {parseInt(this.state.message.id) === item.id &&

{this.state.message.message}

} +``` + +Finally we need to set the new state in the `addToOrder` function + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +If we run our tests now everything should go green. + +Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. + +```bash +$ touch spec/requests/api/client_can_create_new_order_spec.rb +``` + +Add the following test to the spec file + +```ruby +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + it 'responds with success message' do + post '/api/orders', params: {id: product_1.id } + + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end +end +``` +Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. + +Run the test and make sure that you are getting the correct error messages. + +We will probably get an error message that states that we have an unitialized constant Api::OrdersController. + +We will fix this by generating a new orders controller. + +`$ rails generate controller Api::Orders create` + +Run the test again and you will get a new error message. + + +The error message probably has to do with our routes and that there are none. Lets fix that. + +```ruby +Rails.application.routes.draw do + namespace :api do + resources :products, only: [:index] + resources :orders, only: [:create] + end +end +``` + +Our problem now is that we are not getting the right response. But this is expected. + +To fix this we need to add the order to the create action in our Orders controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + +The next step we need to take is to generate a Order model to have something to pull out from the database. + +`$ rails generate model order` + +We also need to associate the the order to the order items that we have, and the products. + +`$ rails generate model OrderItems order:references product:references` + +Go ahead and go through the files that are generated and make sure that there are no problems. + +We will now add specs to test the associations we created between the models. +In the newly generated `order_items_spec.rb` go ahead and add two specs. +You will need more than that but for this guide we will limit ourselves to these specs. + +```ruby +require 'rails_helper' + +RSpec.describe OrderItem, type: :model do + it { is_expected.to belong_to :order } + it { is_expected.to belong_to :product } +end +``` + +`order_spec.rb` + +```ruby +require 'rails_helper' + +RSpec.describe Order, type: :model do + it {is_expected.to have_many :order_items} +end +``` + +We need to make sure that the associations are present as well. + +`order_item.rb` + +```ruby +class OrderItem < ApplicationRecord + belongs_to :order + belongs_to :product +end +``` + +`order.rb` + +```ruby +class Order < ApplicationRecord + has_many :order_items +end +``` + +Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. + + +------ + +### Update the order Backend [cart react p3] + +First we need to amend our previous spec. + +We will amend our specs by adding a before block +```ruby + before do + post '/api/orders', params: { id: product_1.id } + @order = Order.last + end +``` + +And adding a new spec + +```ruby + it 'adds another product to order if param "order_id" is present' do + post '/api/orders', params {id: product_2.id, order_id: @order.id} + expect(@order.order_items.count).to eq 2 + end +``` +In order to get this to work in the first iteration we can simply refactor our create action in the controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = if params[:order_id] + Order.find(params[:order_id]) + else + Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + +Run your test and make sure that this is going green. This is working however this is not the best practice. + +Let's make sure that we follow the conventions and refactor this code so it works. + +Lets refactor our test first +```ruby + it 'adds another product to order if param "order_id" is present' do + put "/api/orders/#{order.id}", params {product_id: product_2.id } + expect(@order.order_items.count).to eq 2 + end +``` + +You will probably get an routing error. We will fix that with adding update to the routes: + + `resources :orders, only: [:create, :update]` + +Run the test again and you will get a error stating that there is no action update. + +```ruby + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: { message: 'The product has been added to your order'} + end +``` + +And remember to clean out the create action to its previous state: + +```ruby + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order' } + end +``` + +We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. + +```rb +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + before do + post '/api/orders', params: { product_id: product_1.id } + @order_id = JSON.parse(response.body)['order_id'] + end + + describe 'POST /api/orders' do + it 'responds with success message' do + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end + + it 'responds with order id' do + order = Order.find(@order_id) + expect(JSON.parse(response.body)['order_id']).to eq order.id + end + end + + describe 'PUT /api/orders/:id' do + before do + put "/api/orders/#{@order_id}", params: { product_id: product_2.id } + @order = Order.find(@order_id) + end + + it 'adds another product to order if request is a PUT and param id of the order is present' do + expect(@order.order_items.count).to eq 2 + end + + it 'responds with order id' do + expect(JSON.parse(response.body)['order_id']).to eq @order.id + end + end +end +``` + +And in the controller: + +```rb +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + + +### Putting it togheter + +In order for the client to work with the backend we need to make some adjustment to the code. +In your clinent make sure that you add product_id instead of the id. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +### Returning to the client + +Let's go back tot the client and add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. + +We need to do a couple of things: + +Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. + +We will also add a `PUT` request to this block in order to update the order that has already been created. + +And at last we will add a test to check that we can add multiple products to an order. + +The final form of the feature will be the following: + +```js +describe('User can add a product to his/her order', () => { + + beforeEach(() => { + cy.server(); + cy.route({ + method: 'GET', + url: 'http://localhost:3000/api/products', + response: 'fixture:product_data.json' + }) + + cy.route({ + method: 'POST', + url: 'http://localhost:3000/api/orders', + response: { message: 'The product has been added to your order', order_id: 1 } + }) + + cy.route({ + method: 'PUT', + url: 'http://localhost:3000/api/orders/1', + response: { message: 'The product has been added to your order', order_id: 1 } + }) + cy.visit('http://localhost:3001') +}); + + it('user get a confirmation message when adding product to order', () => { + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +Make sure that you have added the product_id to the messages in the `beforeEach` function. + +Lets continue with the implementation code to get all of this to work. +add the `orderId` to the state +```js +state = { + productData: [], + message: {}, + orderId: '' + } +``` + +Now we need to update the `addToOrder` function with the new `orderId` state. + + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) +} +``` +The `order_id` is what is being returned to us from the backend. + +We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. + +```js + async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderId !== "") { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + } + this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) + } +``` + +So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. + +Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. + + +Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. + +We want to make sure that we can view the products that we have added to our order. + +First we want to make sure that there is a button where we can view the order. +However this button should only be visable when we have added products to our order. + +Refactor your code to look like this: + +```js + [....] + + it('user can add multiple products to order and view its content', () => { + cy.get('button').contains('View order').should('not.exist') // Add this line + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').should('exist') // Add this line + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +And now for the implementation, let's add the button in the `DisplayProducts` component + +```js +return ( + <> + {this.state.orderId !== '' && + } + {this.state.showOrder && +
    + {orderDetailsDisplay} +
+ } + {dataIndex} + +``` + +### React p5 + +Lets work on the view order + +Go to the client, first we willl write the test + +```js + it('user can add multiple product to order and view its content', () => { + cy.get('button').contains('View order').should('not.exist') + + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').should('exist') + + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').click() + cy.get('#order-details').within(()=> { + cy.get('li').should('have.length', 2) + }) + cy.get('button').contains('View order').click() + cy.get('#order-details').should('not.exist') + + }); + ``` + + Run the test and it is failing. + + ```js +return ( + <> + {this.state.orderDetails.hasOwnProperty('products') && + + } + {this.state.showOrder && +
    + {orderDetailsDisplay} +
+ } + {dataIndex} + +``` +check if the tests are going green + +commit now +zap i should have committed a loong time ago + +Lets move go back to our backend to continue this functionality + + + From 09fccdb254b7aed5fd330633280f1f6526ef36fe Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Mon, 16 Mar 2020 07:55:57 +0100 Subject: [PATCH 07/19] Create react-cart-final.md --- react-cart-final.md | 1130 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1130 insertions(+) create mode 100644 react-cart-final.md diff --git a/react-cart-final.md b/react-cart-final.md new file mode 100644 index 00000000..ba1d9848 --- /dev/null +++ b/react-cart-final.md @@ -0,0 +1,1130 @@ +# Adding checkout functionality to your React application + +``` +This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. +If there are typos or issues in this guide, please notify a coach +``` + +In this section we are going to add checkout functionality to our react application. +The prerequsitet for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaning code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. + +Start with making sure that you have the latest code pulled from github on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. + +As always we will work in a test driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. + + +Create a new feature file by typing +`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` + +Add the following tests to the file: + +```js +describe("User can add a product to their order", () => { + before(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: { message: "A product has been added to your order" } + }); + }); + + it("user gests a confirmation messsage when adding a a producut to order", () => { + cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API + cy.get("#product-1").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.wait(3000) + }); +}); +``` +So what are we excpecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? + +When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. + +In our case we already have a component that we use in order to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. + +Let's add the implementation code. + +We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are maping over the products. + +```jsx +
+ {`${item.name} ${item.description} ${item.price}`} + +
+``` +So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. + +Add the following code to the component: + +```js +addToOrder() { + debugger +} +``` + +So why are we adding a debugger here? Well first we need to make sure that the `addToOrder` button actually gets invoked. + +Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome broweser. + +If we can see that then its time for us to add the real functionality. +But first lets make sure that we are hitting the right button. +Go to your chrome console where the execution of the code has stopped and run the following command in the console: + +`event.target.parentElement` + +You should probably see something like this in your console: + +```html +
+ "Pizza" + "Crusty and fluffy" + "100 SEK" + +
+``` +If not then make sure that you are iterating over your products properly. + +So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it beacuse the values in the product are sent back to us in the form of an object. + +Add the dataset to the parent div: + +```js +
+ {`${item.name} ${item.description} ${item.price}`} + +
+``` + +Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. +you should see the same information as before, but the information about the product will be returend to us in the form of an object. + +This is good new for us beacuse we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) +} +``` + +So what did we do here? First we made the method asyncronous beacuse we need to wait for a response from our API, we passed in the event as an argument to the function and we also added axios in order to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` + +Make sure that you add axios and also import it to the top of the file if you haven't already. + +The next thing we are going to focus on is to get the message to be displayed. + +Lets start with adding the message to the state: + +```js +state = { + productData: [], + message: {} + } +``` + +Next we need to display the message below the button. + +```js +[....] + + {parseInt(this.state.message.id) === item.id &&

{this.state.message.message}

} +``` + +Finally we need to set the new state in the `addToOrder` function + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +If we run our tests now everything should go green. + +Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. + +```bash +$ touch spec/requests/api/client_can_create_new_order_spec.rb +``` + +Add the following test to the spec file + +```ruby +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + it 'responds with success message' do + post '/api/orders', params: {id: product_1.id } + + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end +end +``` +Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. + +Run the test and make sure that you are getting the correct error messages. + +We will probably get an error message that states that we have an unitialized constant Api::OrdersController. + +We will fix this by generating a new orders controller. + +`$ rails generate controller Api::Orders create` + +Run the test again and you will get a new error message. + + +The error message probably has to do with our routes and that there are none present. Let's fix that. + +```ruby +Rails.application.routes.draw do + namespace :api do + resources :products, only: [:index] + resources :orders, only: [:create] + end +end +``` + +Our problem now is that we are not getting the right response. But this is expected. + +To fix this we need to add the order to the create action in our Orders controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + +The next step we need to take is to generate a Order model to have something to pull out from the database. + +`$ rails generate model order` + +We also need to associate the the order to the order items that we have, and the products. + +`$ rails generate model OrderItems order:references product:references` + +Go ahead and go through the files that are generated and make sure that there are no problems. + +We will now add specs to test the associations we created between the models. +In the newly generated `order_items_spec.rb` go ahead and add two specs. +You will need more than that but for this guide we will limit ourselves to these specs. + +```ruby +require 'rails_helper' + +RSpec.describe OrderItem, type: :model do + it { is_expected.to belong_to :order } + it { is_expected.to belong_to :product } +end +``` + +`order_spec.rb` + +```ruby +require 'rails_helper' + +RSpec.describe Order, type: :model do + it {is_expected.to have_many :order_items} +end +``` + +We need to make sure that the associations are present as well. + +`order_item.rb` + +```ruby +class OrderItem < ApplicationRecord + belongs_to :order + belongs_to :product +end +``` + +`order.rb` + +```ruby +class Order < ApplicationRecord + has_many :order_items +end +``` + +Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. + +Done here and working + +------ + +## Update the order on the Backend + +First we need to amend our request spec. + +We will amend our specs by adding a before block +```ruby + before do + post '/api/orders', params: { id: product_1.id } + @order = Order.last + end +``` + +And adding a new spec + +```ruby + it 'adds another product to order if param "order_id" is present' do + post '/api/orders', params {id: product_2.id, order_id: @order.id} + expect(@order.order_items.count).to eq 2 + end +``` + + +In order to get this to work in the first iteration we can simply refactor our create action in the controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = if params[:order_id] + Order.find(params[:order_id]) + else + Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + +Run your test and make sure that this is going green. This is working however this is not the best practice. + +Let's make sure that we follow the conventions and refactor this code so it works. + + +Lets refactor our test first +```ruby + it 'adds another product to order if param "order_id" is present' do + put "/api/orders/#{order.id}", params: {product_id: product_2.id } + expect(@order.order_items.count).to eq 2 + end +``` + +You will probably get an routing error. We will fix that with adding update to the routes: + + `resources :orders, only: [:create, :update]` + +Run the test again and you will get a error stating that there is no action update. + +```ruby + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: { message: 'The product has been added to your order'} + end +``` + +And remember to clean out the create action to its previous state: + +```ruby + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order' } + end +``` + +We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. + +```rb +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + before do + post '/api/orders', params: { product_id: product_1.id } + @order_id = JSON.parse(response.body)['order_id'] + end + + describe 'POST /api/orders' do + it 'responds with success message' do + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end + + it 'responds with order id' do + order = Order.find(@order_id) + expect(JSON.parse(response.body)['order_id']).to eq order.id + end + end + + describe 'PUT /api/orders/:id' do + before do + put "/api/orders/#{@order_id}", params: { product_id: product_2.id } + @order = Order.find(@order_id) + end + + it 'adds another product to order if request is a PUT and param id of the order is present' do + expect(@order.order_items.count).to eq 2 + end + + it 'responds with order id' do + expect(JSON.parse(response.body)['order_id']).to eq @order.id + end + end +end +``` + +And in the controller: + +```rb +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + + +### Putting it togheter + +In order for the client to work with the backend we need to make some adjustment to the code. +In your clinent make sure that you add product_id instead of the id. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +## Adding multiple products to the order + + +Let's add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. + +We need to do a couple of things: + +Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. + +We will also add a `PUT` request to this block in order to update the order that has already been created. And at last we will add a test to check that we can add multiple products to an order. + +The final form of the feature will be the following: + +```js +describe('User can add a product to his/her order', () => { + + beforeEach(() => { + cy.server(); + cy.route({ + method: 'GET', + url: 'http://localhost:3000/api/products', + response: 'fixture:product_data.json' + }) + + cy.route({ + method: 'POST', + url: 'http://localhost:3000/api/orders', + response: { message: 'The product has been added to your order', order_id: 1 } + }) + + cy.route({ + method: 'PUT', + url: 'http://localhost:3000/api/orders/1', + response: { message: 'The product has been added to your order', order_id: 1 } + }) + cy.visit('http://localhost:3001') +}); + + it('user get a confirmation message when adding product to order', () => { + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +Make sure that you have added the product_id to the messages in the `beforeEach` function. + +Let's continue with the implementation code to get all of this to work. +add the `orderId` to the state +```js +state = { + productData: [], + message: {}, + orderId: '' + } +``` + +Now we need to update the `addToOrder` function with the new `orderId` state. + + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) +} +``` +The `order_id` is what is being returned to us from the backend. + +We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. + +```js + async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderId !== "") { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + } + this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) + } +``` + +So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. + +Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. + + +Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. + +We want to make sure that we can view the products that we have added to our order. + +First we want to make sure that there is a button where we can view the order. +However this button should only be visable when we have added products to our order. + +Refactor your code to look like this: + +```js + [....] + + it('user can add multiple products to order and view its content', () => { + cy.get('button').contains('View order').should('not.exist') // Add this line + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').should('exist') // Add this line + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +And now for the implementation, let's add the button in the `DisplayProducts` component + +```js +return ( + <> + {this.state.orderId !== '' && } + {dataIndex} + +) +``` + +Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. + +We are working in very small chunks at the time ensuring that we always are making progress as we move along. + +Now it's time for adding the view order functionality to the button. + +As always we start with the test. + +```js + cy.get('button').contains('View order').click() + cy.get('#order-details').within(()=> { + cy.get('li').should('have.length', 2)}) + + cy.get('button').contains('View order').click() + cy.get('#order-details').should('not.exist') +``` + +And ow for the implementation code. First we update our state object + +```js + state = { + productData: [], + message: {}, + orderId: '', + showOrder: false + } +``` +And then the button +```js +return ( + <> + {this.state.orderDetails.hasOwnProperty('products') && + + } + {this.state.showOrder && +
    + {orderDetailsDisplay} +
+ } + {dataIndex} + +``` + +## Viewing the order + +Let's work on the view order + +Go to the client, first we willl write the test + +```js + it('user can add multiple product to order and view its content', () => { + cy.get('button').contains('View order').should('not.exist') + + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').should('exist') + + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').click() + cy.get('#order-details').within(()=> { + cy.get('li').should('have.length', 2) + }) + cy.get('button').contains('View order').click() + cy.get('#order-details').should('not.exist') + + }); + ``` + + Run the test and it is failing.... + Add the following code to the return block of your component + + ```js + return ( + <> + {this.state.orderId !== "" + && } + {this.state.showOrder &&
    +
  • Item 1
  • +
  • Item 2
  • +
} + {dataIndex} + + ); +``` +Check if the tests are going green, The last one is probably not. However we have hard coded the values here. The best approach should be to pull in this information instead. + +Lets go back to our test and refactor them a little + +Add the fixture files to the test + +```bash +$ touch cypress/fixtures/put_response.json +$ touch cypress/fixtures/post_response.json +``` + + +zap i should have committed a loong time ago +(update this with the code from the client) + + +### Serializers +Lets move go back to our backend to continue this functionality + +We want to add a gem called Active Model Serializer +So what is a serializer? A serializer makes sure that our json responses has particular shape and content, rather than the content provided by the built in json responses. We want to be in control of the information that is leaving the application, and in order to do that we are using this gem. + +Add the gem to your `Gemfile` and run bundle + +```rb +gem 'active_model_serializers' +``` + +Create the file a configuration file where we are adding the settings. +```bash +$ touch config/initializers/active_model_serializers.rb +``` +Add + +```rb +ActiveModelSerializers.config.adapter = :json +``` + +Now lets generate a new serializer. In the future a new serializer will be generated by default if you generate a new model. But for our existing model we will run the generator. + +```bash +$ rails g serializer order +``` + +This generator creates a new file. But before we add code to this (and there is a lot of code to add) we need to refactor our test and implementation to match the serialization. + +Go over the following code carefully and take note of the changes we are making. + +`spec/requests/api/client_can_create_new_order_spec.rb` + +```rb +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: "Pizza", price: 10) } + let!(:product_2) { create(:product, name: "Kebab", price: 20) } + + before do + post "/api/orders", params: { product_id: product_1.id } + order_id = JSON.parse(response.body)["order"]["id"] + @order = Order.find(order_id) + end + + describe "POST /api/orders" do + it "responds with success message" do + expect(JSON.parse(response.body)["message"]).to eq "The product has been added to your order" + end + + it "responds with order id" do + expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id + end + + it "responds with right amount of products" do + expect(JSON.parse(response.body)["order"]["products"].count).to eq 1 + end + + it "responds with right order total" do + expect(JSON.parse(response.body)["order"]["total"]).to eq 10 + end + end + + describe "PUT /api/orders/:id" do + before do + put "/api/orders/#{@order.id}", params: { product_id: product_2.id } + put "/api/orders/#{@order.id}", params: { product_id: product_2.id } + end + + it "adds another product to order if request is a PUT and param id of the order is present" do + expect(@order.order_items.count).to eq 3 + end + + it "responds with order id" do + expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id + end + + it "responds with right amount of unique products" do + expect(JSON.parse(response.body)["order"]["products"].count).to eq 2 + end + + it "responds with right order total" do + expect(JSON.parse(response.body)["order"]["total"]).to eq 50 + end + end +end +``` + +And now for the serializer + +`app/serializers/order_serializer.rb` +```rb +class OrderSerializer < ActiveModel::Serializer + attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total + + def products_1 + products = [] + object.order_items.group_by(&:product_id).each do |key, value| + product = Product.find(key) + hash = { amount: value.count, name: product.name, total: (product.price * value.count) } + products.push hash + end + products + end + + def products_2 + products = [] + unique_items = object.order_items.uniq(&:product) + unique_items_count = object.order_items.group_by(&:product_id).map { |key, value| [key, value.size] }.to_h + unique_items.each do |item| + products.push( + amount: unique_items_count[item.product_id], + name: item.product.name, + total: (unique_items_count[item.product_id] * item.product.price) + ) + end + products + end + + def products_3 + object.order_items.group_by(&:product_id).map do |_key, value| + product = value.uniq(&:product_id)[0].product + { amount: value.size, name: product.name, total: (value.size * product.price) } + end + end + alias_method :products, :products_3 + + def total + object.order_items.joins(:product).sum('products.price') + end +end +``` + +And finally we need to update our controller with the changes. + +`app/controllers/api/orders_controller.rb` + +```rb +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: create_json_response(order) + end + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) + end + +private + + def create_json_response(order) + json = { order: OrderSerializer.new(order) } + json.merge!(message: 'The product has been added to your order') + end +end +``` + +## Go back to the frontend + + +Now we want to go back to our frontend and add the changes there as well. We need to make sure that the order totals are showing up. + +As always we first start with our tests. + +```js + cy.get("button") + .contains("View order") + .click(); + cy.get("#order-details").within(() => { + cy.get("li") + .should("have.length", 2) + .first() + .should("have.text", "1 x Salad") + .next() + .should("have.text", "1 x Ice Cream"); + }); +``` + +We also need to import the DisplayProductData to the feature file. + +`import DisplayProductData from '../../src/components/DisplayProductData'` + +And of course we need to update our fixture files to accomodate the new changes + +```json +// put_response.json + +{ + "message": "The product has been added to your order", + "order": { + "id": 1, + "products": [ + { + "amount": 1, + "name": "Salad", + "price": 4.0 + }, + { + "amount": 1, + "name": "Ice Cream", + "price": 3.75 + } + ], + "order_total": 7.75 + } +} +``` + +and + +```json +// post_response.json +{ + "message": "The product has been added to your order", + "order": { + "id": 1, + "products": [ + { + "amount": 1, + "name": "Salad", + "price": 4.0 + } + ], + "order_total": 4.0 + } +} +``` +And Finally our implementation code. + +First in our `addToOrder` function +`this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order })` + +So the function looks like this + +```js + async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderDetails.hasOwnProperty('id')) { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id }) + } + this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order }) +``` + +And in our render function +```js +return
  • {`${item.amount} x ${item.name}`}
  • +``` + +And finally in the return + +```js +return ( + <> + {this.state.orderDetails.hasOwnProperty('products') && + + } + {this.state.showOrder && + <> +
      + {orderDetailsDisplay} +
    +

    To pay: {this.state.orderDetails.order_total}

    + + } + {dataIndex} + +``` + +commit this and lets go back to the backend for finilazing the order. + +first we need to create a new request spec + +`$ touch spec/requests/api/user_can_finalize_order_spec.rb` + +inside of this file add the followign specs + +```rb +RSpec.describe Api::OrdersController, type: :request do + let(:product_1) { create(:product, name: 'Pizza', price: 10) } + let(:product_2) { create(:product, name: 'Kebab', price: 20) } + let(:order) { create(:order) } + + before do + order.order_items.create(product: product_1) + order.order_items.create(product: product_2) + + put "/api/orders/#{order.id}", params: { activity: 'finalize' } + end + + describe 'PUT /api/orders' do + it 'responds with success message' do + expect(JSON.parse(response.body)['message']).to eq 'Your order will be ready in 30 minutes!' + end + + it 'sets the order attribute "finalized" to true' do + expect(order.reload.finalized).to eq true + end + end +end +``` + +Run the specs you should and take notice of the error messages. + +We need to add a new column in our orders database table. + +`rails g migration AddFinalizedToOrders finalized:boolean` + +Before you migrate make sure that it is adding a column to the orders db table and make sure that the default value is set to false. + +It should look like this: + + +```rb +class AddFinalizedToOrders < ActiveRecord::Migration[6.0] + def change + add_column :orders, :finalized, :boolean, default: false + end +end +``` +run the migrations and make sure that the changes have been added to your schema file. + +The final step we need to take is to refactor our update action. + +`app/controllers/api/orders_controller.rb` + +```rb + def update + order = Order.find(params[:id]) + if params[:activity] + order.update_attribute(:finalized, true) + render json: { message: 'Your order will be ready in 30 minutes!' } + else + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) + end + end +``` + +Remember that need to update our order serializer +```rb +attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total, :finalized +``` + + +Run your spec they should be green by now. + +Lets head back over to the client. + +## Finalizing and back to the client + +As always let's start with a new spec. + +Add the following code to your feature test: + +```js +it('user can finalize the order', () => { + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + }) + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + }) + cy.get('button').contains('View order').click() + cy.route({ + method: 'PUT', + url: 'http://localhost:3000/api/orders/1', + response: { message: 'Your order will be ready in 30 minutes!' } + }) + cy.get('button').contains('Confirm!').click() + cy.get('.message').should('contain', "Your order will be ready in 30 minutes!") +}); + +``` + +We also want to make sure that we have som econfigration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. + +```js + +// cypress/support/commands.js +const COMMAND_DELAY = 150; + + +for (const command of ['get', 'visit', 'click', 'trigger', 'type', 'clear', 'reload', 'contains']) { + Cypress.Commands.overwrite(command, (originalFn, ...args) => { + const origVal = originalFn(...args); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(origVal); + }, COMMAND_DELAY); + }); + }); +} +``` + +Now for the implementation code: + +Update the state object with orderTotal +```js +state = { + productData: [], + message: {}, + orderDetails: {}, + showOrder: false, + orderTotal: '' + } +``` + +Add a new asyncronous function to handle the finalize order + +```js + async finalizeOrder() { + let orderTotal = this.state.orderDetails.order_total + let result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { activity: 'finalize' }) + this.setState({ message: { id: 0, message: result.data.message }, orderTotal: orderTotal, orderDetails: {}}) + } +``` +You also need to update the addToOrder function + +```js +if (this.state.orderDetails.hasOwnProperty('id') && this.state.orderDetails.finalized === false) +``` + +And update the return statement with the new changes. + +```js +return ( + <> + {this.state.message.id === 0 && +

    {this.state.message.message}

    + } + {this.state.orderDetails.hasOwnProperty('products') && + + } + {this.state.showOrder && + <> +
      + {orderDetailsDisplay} +
    +

    To pay: {this.state.orderDetails.order_total || this.state.orderTotal} kr

    + + + } + {dataIndex} + + ) +``` + +And finally the render function: + +```js +{`${item.name} ${item.description} - ${item.price}kr `} +``` + +Run your tests now and they should be green. + +We have now finalized the entire creating order functionality both in the front and the backend. + From 4b02c32232ccc709335fab0669017297edf54fe7 Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Mon, 16 Mar 2020 11:39:54 +0100 Subject: [PATCH 08/19] Update react-cart-final.md --- react-cart-final.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/react-cart-final.md b/react-cart-final.md index ba1d9848..6b870770 100644 --- a/react-cart-final.md +++ b/react-cart-final.md @@ -869,7 +869,8 @@ And of course we need to update our fixture files to accomodate the new changes "price": 3.75 } ], - "order_total": 7.75 + "order_total": 7.75, + "finalized": false } } ``` @@ -889,7 +890,8 @@ and "price": 4.0 } ], - "order_total": 4.0 + "order_total": 7.75, + "finalized": false } } ``` From 51d3cfe84c95858995c7f3772597f4125edf5f94 Mon Sep 17 00:00:00 2001 From: Faraz Naeem Date: Thu, 19 Mar 2020 20:23:16 +0100 Subject: [PATCH 09/19] Create stripe_react_rails.md --- stripe_react_rails.md | 586 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 586 insertions(+) create mode 100644 stripe_react_rails.md diff --git a/stripe_react_rails.md b/stripe_react_rails.md new file mode 100644 index 00000000..62149186 --- /dev/null +++ b/stripe_react_rails.md @@ -0,0 +1,586 @@ +This guide needs to have the order functionality from the previous chapter in place. We will continue on the code both from the backend as well as from the frontend. + +We are going to use stripe to finalize the order by paying for the products. + +Stripe is a payment gateway that is developer-friendly. In this section, we are going to set up the functionality that we need in order to make the payments with debit or credit card. + +First and foremost you need to create an account on stripe. We will get back to this later. But make sure that you have an account ready. So let's set the stage, create a new branch and let's get started. + +As wit everything that we develop here at Craft Academy we work in a test-driven way. This means that we always start with our feature test to stay in scope but also make sure that we are not breaking any functionality that has already been added to the previous features. + +Start by creating a new feature by running the following command in your terminal. +`$ touch cypress/integration/userCanMakePayment.feature.js` + +After the file has been created we need to add the following test. + +```js +describe("User can add a product to his/her order", () => { + beforeEach(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: "fixture:post_response.json" + }); + + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: "fixture:put_response.json" + }); + + cy.visit("http://localhost:3001"); + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("button") + .contains("View order") + .click(); + }); + + it("user can pay for his order", () => { + cy.get("button") + .contains("Confirm!") + .click(); + cy.get("#payment-form").should("exist"); + }); +}); +``` +So what is happening in this test? +Well, we are setting the stage and making sure that we can display a payment form. Remember that we always work in small chunks and that we continually refactor both our test but also our implementation code as we add more and more functionality. + +Let's head over to the component that we want to display our payment form. + +Start by adding a new state, where we are adding the payment form. Why are we setting it to false? + +```js +state = { + productData: [], + message: {}, + orderDetails: {}, + showOrder: false, + orderTotal: "", + showPaymentform: false +}; +``` + +After that, we need to refactor the button to show the form when the user clicks on it. + + +```js +; +{ + this.state.showPaymentform && ( +
    + +
    + ); +} +``` +remember to also import the on the top of the file. +But we don't have a PaymentForm component you are thinking. Well, that's right. We don't so... +Let's create one. + +```bash +$ touch src/components/PaymentForm.jsx +``` + +```js +import React, { Component } from "react"; + +class PaymentForm extends Component { + render() { + return ( +
    +

    Here we will show a payment form

    +
    + ); + } +} + +export default PaymentForm; +``` +Run your test, commit and all that fun stuff. +Just to check, you did remember to create a new branch on git before we started right? Of course you did. silly me.... + +Let's revisit our test for one moment. When we are dealing with stripe they provide us with a lot of functionality in terms of security and checks. This is good for development but could be an issue when we are trying to test the payment functionality. We need to test that we can see an iframe where we will be displaying the payment form. + +Refactor your test to look like this. + +```js +it("user can pay for his order", () => { + cy.get("button") + .contains("Confirm!") + .click(); + cy.get("#payment-form").should("exist"); + cy.wait(1000); + cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="cardnumber"]') + .type("4242424242424242", { delay: 50 }); + }); +}); +``` + +We are finding an input field and filling it out with the card number. Small steps remember. Run your test and behold the error messages. + +Time for the implementation + +Add the following package. + +```bash +$ yarn add react-stripe-elements +``` + +To use stripe we also need to add a script provided by stripe. + +Add a script to the head in index.html + +```html + +``` + +And we also need to turn off some of the built-in settings on cypress just to make sure that we are not getting errors because of our browser. + +Setting chromeWebSecurity to false in Chrome-based browsers allows you to do the following: + +- Display insecure content +- Navigate to any superdomain without cross-origin errors +- Access cross-origin iframes that are embedded in your application + +Add `chromeWebSecurity` and set it to `false` + +`cypress.json` + +```json +{ + "baseUrl": "http://localhost:3001", + "chromeWebSecurity": false +} +``` +Head over to your `index.js` and import the StripeProvider. + +```js +import { StripeProvider } from "react-stripe-elements"; +``` + +And wrap component with the StripeProvider + +```js +ReactDOM.render( + + + , + document.getElementById("root") +); +``` + +The StripeProvider gives us access to the Stripe object. The Stripe object will contain the secret, and publishable key which will allow us to access the Stripe API. + +Get your API key from the stripe dashboard, make sure that you select "Show test data" and go to the developer's tab. Copy your Publishable key and add it to the stripe provider in your App component. + +```js +ReactDOM.render( + + + , + document.getElementById("root") +); +``` +Time to start working on the payment form itself. +import + +Wrap the PaymentForm component with the Elements, and make sure that you import Elements from `"react-stripe-elements"` + +```js +{ + this.state.showPaymentform && ( +
    + + + +
    + ); +} +``` + +Move over to your PaymentForm and update it with the following code. + +```js +import React, { Component } from "react"; +import { injectStripe, CardNumberElement } from "react-stripe-elements"; + +class PaymentForm extends Component { + render() { + return ( + <> + + + + ); + } +} + +export default injectStripe(PaymentForm); +``` + +So what are we doing here? We are adding an input field where the users can add their card number but to use that we need to use the injectStripe component to make use of their form elements. + +Run the test now and they should be green. If they do, commit. + +So we have added a rudimentary payment form and we have also added the package that we need to use stripe in our react app. But we need multiple input fields for the rest of our card information. So let's add that now. + + + +We need to update our test again for the remaining input fields, as well as with the assertion that the payment has gone through. + +```js +it("user can pay for his order", () => { + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + body: { activity: "finalize" }, + response: { paid: true, message: "Your order will be ready in 30 minutes!" } + }); + cy.get("button") + .contains("Confirm!") + .click(); + + cy.get("#payment-form").should("exist"); + cy.wait(1000); + cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="cardnumber"]') + .type("4242424242424242", { delay: 50 }); + }); + + cy.get('iframe[name^="__privateStripeFrame6"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="exp-date"]') + .type("1222", { delay: 10 }); + }); + cy.get('iframe[name^="__privateStripeFrame7"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="cvc"]') + .type("999", { delay: 10 }); + }); + + cy.get("button") + .contains("Submit") + .click(); + + cy.get("#payment-form").should("not.exist"); + cy.get(".message").should( + "contain", + "Your order will be ready in 30 minutes!" + ); +}); +``` + +Let us go back to your PaymentForm component and update the return with the rest of the payment fields. Make sure that you import the + +```js + return ( + <> + + + + + + + + +``` + +Next we want to add a function called payWithStripe + +```js +async payWithStripe() { + await this.props.stripe.createToken().then(response => { + if (response.token) { + try { + this.performPayment(response.token) + } + catch { + + } + } + + }) + } +``` + +We also want to add a function called performPayment. This function will make the payment. + +```js + async performPayment(token) { + let orderResponse = await axios.put( + `http://localhost:3000/api/orders/${this.props.orderDetails.id}`, + { + activity: 'finalize', + stripeToken: token + } + ) + if (orderResponse.data.paid === true) { + this.props.finalizeOrder(orderResponse.data.message) + + } else { + debugger + } + } +``` + +And finally, we need to update the button + +```js + +``` + +Make sure that you have made adequate imports to the component. + +Move over to the DisplayProduct component or your main component where you are adding the products to the order. + +Update the Props that you are sending through + +```js + +``` + +And update the finalizeOrder function we are passing in the finializeOrder function as a prop to the component. + +```js + async finalizeOrder(message) { + let orderTotal = this.state.orderDetails.order_total + this.setState({ message: { id: 0, message: message }, orderTotal: orderTotal, orderDetails: {}, showPaymentForm: false }) + } +``` + +Run your tests and they should go green. + +Let's move over to the backend + +## Stripe in the Backend + +Time to work on the backend. Make sure that you create a new branch here as well before you get started. + +Start with creating a request spec. + +```bash +$ touch spec/requests/api/client_can_send_payment_request_spec.rb +``` + +Add the following test + +```rb +RSpec.describe Api::OrdersController, type: :request do + let(:product_1) { create(:product, price: 50) } + let(:order) { create(:order) } + let!(:order_item) { create(:order_item, product: product_1, order: order) } + + before do + put "/api/orders/#{order.id}", + params: { activity: "finalize", + stripetoken: "12345", + email: 'testing@test.com' + } + end + + it '' do + expect(JSON.parse(response.body)).to eq JSON.parse('{"paid": true, "message": "Your order will be ready in 30 minutes!"}') + end +end +``` + +Run your tests and the should be failing. That is a good thing because now we know what to do next. + +Head over to your orders controller and add the following functionality to the update action + +```rb + def update + order = Order.find(params[:id]) + if params[:activity] + payment_status = perform_stripe_payment + if payment_status == true + # successful payment + order.update_attribute(:finalized, true) + render json: { paid: true, message: 'Your order will be ready in 30 minutes!' } + + else + # unsuccessful payment + end + + else + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) + end + end +``` +We are not dealing with the unsuccessful payment. Make sure that you add and test for an error message once we are done with the Happy path. + +You will get a Name error stating that there is no `perform_stripe_payment` + +Let's fix that. In the private section of the orders controller add the following method + +```rb + def perform_stripe_payment + order = Order.find(params[:id]) + customer = Stripe::Customer.create( + email: params[:email], + source: params[:stripeToken], + description: 'slowfood client' + ) + + charge = Stripe::Charge.create( + customer: customer.id, + amount: ordr.order_total, + currency: 'sek' + ) + charge + end +``` +We are calling the Stripe gem to create a customer but also to make charge the customer. + +You will be getting errors now, we need to fix that by adding the stripe gem to our Gemfile. + +`gem 'stripe-rails'` + +Run the tests again and your new error message should something in the lines of + +```bash + Stripe::AuthenticationError: + No API key provided. Set your API key using "Stripe.api_key = ". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions. +``` + +This means that we need to use our API key that Stripe has provided for us. + +Grab the "secret key" from your Stripe dashboard. + +Do not reveal this key. To use this securely we will add the key to our credentials file. Credentials is an encrypted file in rails. We need the master.key file in our config folder to open this file. The master.key is added to your gitignore by default so that you don\t accidentally push it to version control. + +Use the following steps to modify your credentials. + +Head over to your terminal and run the following command. + +```bash +$ EDITOR='code --wait' rails credentials:edit +``` +Notice that your terminal has been locked and the credentials file has been decrypted and opened in your code editor. The terminal will be locked until you save and close the credentials file in your code editor. But before we do that we need to add the secret key and publishable key in this file. + + +```yml +secret_key_base: 50fba2642c2978bcf7536a53606328149f18c7382a070822bf86f0d141095d9ed2c2c472b77577df08c34c61e36f3a27aaba26fb746cfb6a79ce3456edbcb04b +stripe: + pk_key: your_publishable_key_here + secret_key: your_secret_key_here +``` + +Save and close the file and you should be getting the following message in the terminal. + +```bash +16:16 $ EDITOR='code --wait' rails credentials:edit +File encrypted and saved. +``` + +To make use of the stripe credentials we need to add some configurations. + +Head over to the `application.rb` file where we turn off the generators etc and add the following configurations. + +```rb + config.generators do |generate| + generate.helper false + generate.assets false + generate.view_specs false + generate.helper_specs false + generate.routing_specs false + generate.controller_specs false + end + config.stripe.secret_key = Rails.application.credentials.stripe[:secret_key] + config.stripe.publishable_key = Rails.application.credentials.stripe[:pk_key] + end +``` + +We do not want to make actual network calls to stripe when we are in development mode. For that we will use a gem called `stripe-ruby-mock`. Make sure that you use the version stated below. + +Add the following gem to the Gemfile + +`gem 'stripe-ruby-mock', '~> 2.5.6', require: 'stripe_mock'` + +Then we want to update the rails_helper to add the mock functionality + +Add `require 'stripe_mock'` to the rails_helper and add the configuration to RSpec + +```rb +RSpec.configure do |config| + config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! + config.include FactoryBot::Syntax::Methods + config.include(Shoulda::Matchers::ActiveRecord, type: :model) + config.before(:each) do + @stripe_test_helper = StripeMock.create_test_helper + StripeMock.start + end + config.after(:each) do + StripeMock.stop + end +end +``` +Next up we want to modify the test with the mocked data. + +```rb + before do + put "/api/orders/#{order.id}", + params: { activity: "finalize", + stripeToken: StripeMock.create_test_helper.generate_card_token, + email:'user@mail.com'} + end +``` + +And finally update the perform_stripe_payment method in our controller. + +```rb + def perform_stripe_payment + order = Order.find(params[:id]) + customer = Stripe::Customer.create( + email: params[:email], + source: params[:stripeToken], + description: 'slowfood client' + ) + + charge = Stripe::Charge.create( + customer: customer.id, + amount: order.order_total.to_i * 100, + currency: 'sek' + ) + charge.paid + end + ``` + + Run your tests now and they should be going green. + So Stripe added. + Let's celebrate by making a commit! + From 8734d37d0d177dab1cb09a343310715c71b5e60a Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Thu, 2 Apr 2020 12:11:19 +0200 Subject: [PATCH 10/19] Updates the add to order message test --- react-cart-final.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/react-cart-final.md b/react-cart-final.md index 6b870770..00714f77 100644 --- a/react-cart-final.md +++ b/react-cart-final.md @@ -6,11 +6,11 @@ If there are typos or issues in this guide, please notify a coach ``` In this section we are going to add checkout functionality to our react application. -The prerequsitet for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaning code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. +The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. -Start with making sure that you have the latest code pulled from github on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. +Start with making sure that you have the latest code pulled from GitHub on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. -As always we will work in a test driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. +As always we will work in a test-driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. Create a new feature file by typing @@ -36,17 +36,17 @@ describe("User can add a product to their order", () => { }); it("user gests a confirmation messsage when adding a a producut to order", () => { - cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API + cy.visit("/"); cy.get("#product-1").within(() => { cy.get("button") .contains("Add to order") .click(); }); - cy.wait(3000) + cy.contains('A product has been added to your order') }); }); ``` -So what are we excpecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? +So what are we expecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. @@ -54,13 +54,23 @@ In our case we already have a component that we use in order to display the prod Let's add the implementation code. -We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are maping over the products. +We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are mapping over the products. ```jsx -
    - {`${item.name} ${item.description} ${item.price}`} - -
    +dataIndex = ( +
    + {this.state.productData.map(item => { + return ( +
    + {`${item.name} ${item.description} ${item.price}`} + +
    + ); + })} +
    + ); ``` So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. @@ -98,6 +108,10 @@ So this was our first iteration. But we need to make sure that we are pulling th Add the dataset to the parent div: +`data-id={item.id} data-price={item.price}` + +Like this: + ```js
    {`${item.name} ${item.description} ${item.price}`} From 969c0b8515e1e41634477541b167343f868bd41b Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Tue, 7 Apr 2020 08:32:58 +0200 Subject: [PATCH 11/19] Changes structure and fixes typos --- .../02_displaying_messages_and_the_backend.md | 90 ++ .../react_checkout_guide/react-cart-final.md | 766 +++++++++--------- 2 files changed, 468 insertions(+), 388 deletions(-) create mode 100644 guides/react_checkout_guide/02_displaying_messages_and_the_backend.md rename react-cart-final.md => guides/react_checkout_guide/react-cart-final.md (65%) diff --git a/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md b/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md new file mode 100644 index 00000000..d769fba0 --- /dev/null +++ b/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md @@ -0,0 +1,90 @@ +## Displaying messages and the Backend + +The next thing we are going to focus on is to get the message to be displayed. + +Lets start with adding the message to the state: + +```js +state = { + productData: [], + message: {} +}; +``` + +Next we need to display the message below the button. + +```js +[....] + + {parseInt(this.state.message.id) === item.id &&

    {this.state.message.message}

    } +``` + +Finally we need to set the new state in the `addToOrder` function + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +If we run our tests now everything should go green. + +Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. + +```bash +$ touch spec/requests/api/client_can_create_new_order_spec.rb +``` + +Add the following test to the spec file + +```ruby +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + it 'responds with success message' do + post '/api/orders', params: {id: product_1.id } + + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end +end +``` + +Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. + +Run the test and make sure that you are getting the correct error messages. + +We will probably get an error message that states that we have an unitialized constant Api::OrdersController. + +We will fix this by generating a new orders controller. + +`$ rails generate controller Api::Orders create` + +Run the test again and you will get a new error message. + +The error message probably has to do with our routes and that there are none present. Let's fix that. + +```ruby +Rails.application.routes.draw do + namespace :api do + resources :products, only: [:index] + resources :orders, only: [:create] + end +end +``` + +Our problem now is that we are not getting the right response. But this is expected. + +To fix this we need to add the order to the create action in our Orders controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` diff --git a/react-cart-final.md b/guides/react_checkout_guide/react-cart-final.md similarity index 65% rename from react-cart-final.md rename to guides/react_checkout_guide/react-cart-final.md index 00714f77..3c402263 100644 --- a/react-cart-final.md +++ b/guides/react_checkout_guide/react-cart-final.md @@ -1,18 +1,17 @@ -# Adding checkout functionality to your React application +# Adding Checkout functionality to your React application ``` -This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. -If there are typos or issues in this guide, please notify a coach +This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. +If there are typos or issues in this guide, please notify a coach ``` -In this section we are going to add checkout functionality to our react application. -The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. +In this section we are going to add checkout functionality to our react application. +The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. Start with making sure that you have the latest code pulled from GitHub on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. As always we will work in a test-driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. - Create a new feature file by typing `$ touch cypress/integration/userCanAddProductsToOrder.feature.js` @@ -27,52 +26,52 @@ describe("User can add a product to their order", () => { url: "http://localhost:3000/api/products", response: "fixture:product_data.json" }); - + cy.route({ method: "POST", url: "http://localhost:3000/api/orders", response: { message: "A product has been added to your order" } }); }); - + it("user gests a confirmation messsage when adding a a producut to order", () => { - cy.visit("/"); + cy.visit("/"); cy.get("#product-1").within(() => { cy.get("button") .contains("Add to order") .click(); }); - cy.contains('A product has been added to your order') + cy.contains("A product has been added to your order"); }); }); ``` -So what are we expecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? -When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. +So what are we expecting to see in the implementation? Well, we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that, we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? + +When you run your test now it should all fail and to make it work we have to add the actual implementation code. -In our case we already have a component that we use in order to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. +In our case we already have a component that we use to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. -Let's add the implementation code. +Let's add the implementation code. We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are mapping over the products. ```jsx dataIndex = ( -
    - {this.state.productData.map(item => { - return ( -
    - {`${item.name} ${item.description} ${item.price}`} - -
    - ); - })} +
    + {this.state.productData.map(item => { + return ( +
    + {`${item.name} ${item.description} ${item.price}`} +
    ); + })} +
    +); ``` -So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. + +So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. Add the following code to the component: @@ -82,12 +81,14 @@ addToOrder() { } ``` -So why are we adding a debugger here? Well first we need to make sure that the `addToOrder` button actually gets invoked. +So why are we adding a debugger here? Well, first we need to make sure that the `addToOrder` button gets invoked. + +Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome browser. -Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome broweser. +If we can see that then its time for us to add the real functionality. + +But first let's make sure that we are hitting the right button. -If we can see that then its time for us to add the real functionality. -But first lets make sure that we are hitting the right button. Go to your chrome console where the execution of the code has stopped and run the following command in the console: `event.target.parentElement` @@ -96,137 +97,54 @@ You should probably see something like this in your console: ```html
    - "Pizza" - "Crusty and fluffy" - "100 SEK" + "Pizza" "Crusty and fluffy" "100 SEK"
    ``` -If not then make sure that you are iterating over your products properly. -So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it beacuse the values in the product are sent back to us in the form of an object. +If not then make sure that you are iterating over your products properly. +So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it because the values in the product are sent back to us in the form of an object. Add the dataset to the parent div: - `data-id={item.id} data-price={item.price}` -Like this: +Like this: ```js -
    - {`${item.name} ${item.description} ${item.price}`} - -
    +
    + {`${item.name} ${item.description} ${item.price}`} + +
    ``` Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. -you should see the same information as before, but the information about the product will be returend to us in the form of an object. - -This is good new for us beacuse we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) -} -``` - -So what did we do here? First we made the method asyncronous beacuse we need to wait for a response from our API, we passed in the event as an argument to the function and we also added axios in order to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` -Make sure that you add axios and also import it to the top of the file if you haven't already. +you should see the same information as before, but the information about the product will be returned to us in the form of an object. -The next thing we are going to focus on is to get the message to be displayed. - -Lets start with adding the message to the state: - -```js -state = { - productData: [], - message: {} - } -``` - -Next we need to display the message below the button. - -```js -[....] - - {parseInt(this.state.message.id) === item.id &&

    {this.state.message.message}

    } -``` - -Finally we need to set the new state in the `addToOrder` function +This is good news for us because we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. ```js async addToOrder(event) { let id = event.target.parentElement.dataset.id let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) - this.setState({message: {id: id, message: result.data.message}}) } ``` -If we run our tests now everything should go green. - -Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. - -```bash -$ touch spec/requests/api/client_can_create_new_order_spec.rb -``` - -Add the following test to the spec file - -```ruby -RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: 'Pizza') } - let!(:product_2) { create(:product, name: 'Kebab') } - - it 'responds with success message' do - post '/api/orders', params: {id: product_1.id } - - expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' - end -end -``` -Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. - -Run the test and make sure that you are getting the correct error messages. - -We will probably get an error message that states that we have an unitialized constant Api::OrdersController. - -We will fix this by generating a new orders controller. - -`$ rails generate controller Api::Orders create` - -Run the test again and you will get a new error message. - +So what did we do here? First, we made the method asynchronous because we need to wait for a response from our API, we passed in the event as an argument to the function and we also added `axios` to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` -The error message probably has to do with our routes and that there are none present. Let's fix that. +Make sure that you add `axios` and also import it to the top of the file if you haven't already. -```ruby -Rails.application.routes.draw do - namespace :api do - resources :products, only: [:index] - resources :orders, only: [:create] - end -end -``` - -Our problem now is that we are not getting the right response. But this is expected. +---------------------------- -To fix this we need to add the order to the create action in our Orders controller. - -```ruby -class Api::OrdersController < ApplicationController - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` -The next step we need to take is to generate a Order model to have something to pull out from the database. +The next step we need to take is to generate a Order model to have something to pull out from the database. -`$ rails generate model order` +`$ rails generate model order` We also need to associate the the order to the order items that we have, and the products. @@ -234,9 +152,9 @@ We also need to associate the the order to the order items that we have, and the Go ahead and go through the files that are generated and make sure that there are no problems. -We will now add specs to test the associations we created between the models. -In the newly generated `order_items_spec.rb` go ahead and add two specs. -You will need more than that but for this guide we will limit ourselves to these specs. +We will now add specs to test the associations we created between the models. +In the newly generated `order_items_spec.rb` go ahead and add two specs. +You will need more than that but for this guide we will limit ourselves to these specs. ```ruby require 'rails_helper' @@ -280,13 +198,14 @@ Run your migrations and then run your specs. Everything should be green like Bru Done here and working ------- +--- ## Update the order on the Backend First we need to amend our request spec. We will amend our specs by adding a before block + ```ruby before do post '/api/orders', params: { id: product_1.id } @@ -294,7 +213,7 @@ We will amend our specs by adding a before block end ``` -And adding a new spec +And adding a new spec ```ruby it 'adds another product to order if param "order_id" is present' do @@ -303,8 +222,7 @@ And adding a new spec end ``` - -In order to get this to work in the first iteration we can simply refactor our create action in the controller. +In order to get this to work in the first iteration we can simply refactor our create action in the controller. ```ruby class Api::OrdersController < ApplicationController @@ -319,12 +237,12 @@ class Api::OrdersController < ApplicationController end ``` -Run your test and make sure that this is going green. This is working however this is not the best practice. - -Let's make sure that we follow the conventions and refactor this code so it works. +Run your test and make sure that this is going green. This is working however this is not the best practice. +Let's make sure that we follow the conventions and refactor this code so it works. Lets refactor our test first + ```ruby it 'adds another product to order if param "order_id" is present' do put "/api/orders/#{order.id}", params: {product_id: product_2.id } @@ -334,7 +252,7 @@ Lets refactor our test first You will probably get an routing error. We will fix that with adding update to the routes: - `resources :orders, only: [:create, :update]` +`resources :orders, only: [:create, :update]` Run the test again and you will get a error stating that there is no action update. @@ -358,7 +276,7 @@ And remember to clean out the create action to its previous state: end ``` -We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. +We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. ```rb RSpec.describe Api::OrdersController, type: :request do @@ -417,10 +335,9 @@ class Api::OrdersController < ApplicationController end ``` - ### Putting it togheter -In order for the client to work with the backend we need to make some adjustment to the code. +In order for the client to work with the backend we need to make some adjustment to the code. In your clinent make sure that you add product_id instead of the id. ```js @@ -433,71 +350,85 @@ async addToOrder(event) { ## Adding multiple products to the order - -Let's add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. +Let's add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. We need to do a couple of things: -Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. +Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. -We will also add a `PUT` request to this block in order to update the order that has already been created. And at last we will add a test to check that we can add multiple products to an order. +We will also add a `PUT` request to this block in order to update the order that has already been created. And at last we will add a test to check that we can add multiple products to an order. The final form of the feature will be the following: ```js -describe('User can add a product to his/her order', () => { - - beforeEach(() => { - cy.server(); - cy.route({ - method: 'GET', - url: 'http://localhost:3000/api/products', - response: 'fixture:product_data.json' - }) - - cy.route({ - method: 'POST', - url: 'http://localhost:3000/api/orders', - response: { message: 'The product has been added to your order', order_id: 1 } - }) - - cy.route({ - method: 'PUT', - url: 'http://localhost:3000/api/orders/1', - response: { message: 'The product has been added to your order', order_id: 1 } - }) - cy.visit('http://localhost:3001') -}); +describe("User can add a product to his/her order", () => { + beforeEach(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); - it('user get a confirmation message when adding product to order', () => { - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: { + message: "The product has been added to your order", + order_id: 1 + } + }); - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - }); + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: { + message: "The product has been added to your order", + order_id: 1 + } + }); + cy.visit("http://localhost:3001"); + }); + + it("user get a confirmation message when adding product to order", () => { + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + }); }); ``` -Make sure that you have added the product_id to the messages in the `beforeEach` function. +Make sure that you have added the product_id to the messages in the `beforeEach` function. Let's continue with the implementation code to get all of this to work. add the `orderId` to the state + ```js state = { - productData: [], - message: {}, - orderId: '' - } + productData: [], + message: {}, + orderId: "" +}; ``` Now we need to update the `addToOrder` function with the new `orderId` state. - ```js async addToOrder(event) { let id = event.target.parentElement.dataset.id @@ -505,9 +436,10 @@ async addToOrder(event) { this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) } ``` -The `order_id` is what is being returned to us from the backend. -We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. +The `order_id` is what is being returned to us from the backend. + +We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. ```js async addToOrder(event) { @@ -522,17 +454,16 @@ We also need to make sure that we are adding a order to an existing one, if one } ``` -So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. +So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. - Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. -We want to make sure that we can view the products that we have added to our order. +We want to make sure that we can view the products that we have added to our order. -First we want to make sure that there is a button where we can view the order. -However this button should only be visable when we have added products to our order. +First we want to make sure that there is a button where we can view the order. +However this button should only be visable when we have added products to our order. Refactor your code to look like this: @@ -540,12 +471,12 @@ Refactor your code to look like this: [....] it('user can add multiple products to order and view its content', () => { - cy.get('button').contains('View order').should('not.exist') // Add this line + cy.get('button').contains('View order').should('not.exist') // Add this line cy.get('#product-2').within(() => { cy.get('button').contains('Add to order').click() cy.get('.message').should('contain', "The product has been added to your order") }) - + cy.get('button').contains('View order').should('exist') // Add this line cy.get('#product-3').within(() => { cy.get('button').contains('Add to order').click() @@ -555,45 +486,52 @@ Refactor your code to look like this: }); ``` -And now for the implementation, let's add the button in the `DisplayProducts` component +And now for the implementation, let's add the button in the `DisplayProducts` component ```js return ( - <> - {this.state.orderId !== '' && } - {dataIndex} - -) -``` + <> + {this.state.orderId !== "" && } + {dataIndex} + +); +``` -Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. +Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. -We are working in very small chunks at the time ensuring that we always are making progress as we move along. +We are working in very small chunks at the time ensuring that we always are making progress as we move along. -Now it's time for adding the view order functionality to the button. +Now it's time for adding the view order functionality to the button. As always we start with the test. ```js - cy.get('button').contains('View order').click() - cy.get('#order-details').within(()=> { - cy.get('li').should('have.length', 2)}) - - cy.get('button').contains('View order').click() - cy.get('#order-details').should('not.exist') +cy.get("button") + .contains("View order") + .click(); +cy.get("#order-details").within(() => { + cy.get("li").should("have.length", 2); +}); + +cy.get("button") + .contains("View order") + .click(); +cy.get("#order-details").should("not.exist"); ``` And ow for the implementation code. First we update our state object ```js - state = { - productData: [], - message: {}, - orderId: '', - showOrder: false - } +state = { + productData: [], + message: {}, + orderId: "", + showOrder: false +}; ``` + And then the button + ```js return ( <> @@ -607,60 +545,84 @@ return ( } {dataIndex} -``` +``` ## Viewing the order -Let's work on the view order +Let's work on the view order Go to the client, first we willl write the test ```js - it('user can add multiple product to order and view its content', () => { - cy.get('button').contains('View order').should('not.exist') +it("user can add multiple product to order and view its content", () => { + cy.get("button") + .contains("View order") + .should("not.exist"); - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('button').contains('View order').should('exist') + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) + cy.get("button") + .contains("View order") + .should("exist"); - cy.get('button').contains('View order').click() - cy.get('#order-details').within(()=> { - cy.get('li').should('have.length', 2) - }) - cy.get('button').contains('View order').click() - cy.get('#order-details').should('not.exist') + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); - }); - ``` + cy.get("button") + .contains("View order") + .click(); + cy.get("#order-details").within(() => { + cy.get("li").should("have.length", 2); + }); + cy.get("button") + .contains("View order") + .click(); + cy.get("#order-details").should("not.exist"); +}); +``` - Run the test and it is failing.... - Add the following code to the return block of your component +Run the test and it is failing.... +Add the following code to the return block of your component - ```js - return ( - <> - {this.state.orderId !== "" - && } - {this.state.showOrder &&
      -
    • Item 1
    • -
    • Item 2
    • -
    } - {dataIndex} - - ); +```js +return ( + <> + {this.state.orderId !== "" && ( + + )} + {this.state.showOrder && ( +
      +
    • Item 1
    • +
    • Item 2
    • +
    + )} + {dataIndex} + +); ``` -Check if the tests are going green, The last one is probably not. However we have hard coded the values here. The best approach should be to pull in this information instead. + +Check if the tests are going green, The last one is probably not. However we have hard coded the values here. The best approach should be to pull in this information instead. Lets go back to our test and refactor them a little @@ -671,42 +633,43 @@ $ touch cypress/fixtures/put_response.json $ touch cypress/fixtures/post_response.json ``` - -zap i should have committed a loong time ago +zap i should have committed a loong time ago (update this with the code from the client) - ### Serializers + Lets move go back to our backend to continue this functionality We want to add a gem called Active Model Serializer -So what is a serializer? A serializer makes sure that our json responses has particular shape and content, rather than the content provided by the built in json responses. We want to be in control of the information that is leaving the application, and in order to do that we are using this gem. +So what is a serializer? A serializer makes sure that our json responses has particular shape and content, rather than the content provided by the built in json responses. We want to be in control of the information that is leaving the application, and in order to do that we are using this gem. -Add the gem to your `Gemfile` and run bundle +Add the gem to your `Gemfile` and run bundle ```rb gem 'active_model_serializers' ``` -Create the file a configuration file where we are adding the settings. +Create the file a configuration file where we are adding the settings. + ```bash $ touch config/initializers/active_model_serializers.rb ``` + Add ```rb ActiveModelSerializers.config.adapter = :json ``` -Now lets generate a new serializer. In the future a new serializer will be generated by default if you generate a new model. But for our existing model we will run the generator. +Now lets generate a new serializer. In the future a new serializer will be generated by default if you generate a new model. But for our existing model we will run the generator. ```bash $ rails g serializer order -``` +``` This generator creates a new file. But before we add code to this (and there is a lot of code to add) we need to refactor our test and implementation to match the serialization. -Go over the following code carefully and take note of the changes we are making. +Go over the following code carefully and take note of the changes we are making. `spec/requests/api/client_can_create_new_order_spec.rb` @@ -752,7 +715,7 @@ RSpec.describe Api::OrdersController, type: :request do it "responds with order id" do expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id end - + it "responds with right amount of unique products" do expect(JSON.parse(response.body)["order"]["products"].count).to eq 2 end @@ -767,6 +730,7 @@ end And now for the serializer `app/serializers/order_serializer.rb` + ```rb class OrderSerializer < ActiveModel::Serializer attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total @@ -839,26 +803,25 @@ end ## Go back to the frontend +Now we want to go back to our frontend and add the changes there as well. We need to make sure that the order totals are showing up. -Now we want to go back to our frontend and add the changes there as well. We need to make sure that the order totals are showing up. - -As always we first start with our tests. +As always we first start with our tests. ```js - cy.get("button") - .contains("View order") - .click(); - cy.get("#order-details").within(() => { - cy.get("li") - .should("have.length", 2) - .first() - .should("have.text", "1 x Salad") - .next() - .should("have.text", "1 x Ice Cream"); - }); +cy.get("button") + .contains("View order") + .click(); +cy.get("#order-details").within(() => { + cy.get("li") + .should("have.length", 2) + .first() + .should("have.text", "1 x Salad") + .next() + .should("have.text", "1 x Ice Cream"); +}); ``` -We also need to import the DisplayProductData to the feature file. +We also need to import the DisplayProductData to the feature file. `import DisplayProductData from '../../src/components/DisplayProductData'` @@ -866,26 +829,26 @@ And of course we need to update our fixture files to accomodate the new changes ```json // put_response.json - + { - "message": "The product has been added to your order", - "order": { - "id": 1, - "products": [ - { - "amount": 1, - "name": "Salad", - "price": 4.0 - }, - { - "amount": 1, - "name": "Ice Cream", - "price": 3.75 - } - ], - "order_total": 7.75, - "finalized": false - } + "message": "The product has been added to your order", + "order": { + "id": 1, + "products": [ + { + "amount": 1, + "name": "Salad", + "price": 4.0 + }, + { + "amount": 1, + "name": "Ice Cream", + "price": 3.75 + } + ], + "order_total": 7.75, + "finalized": false + } } ``` @@ -894,22 +857,23 @@ and ```json // post_response.json { - "message": "The product has been added to your order", - "order": { - "id": 1, - "products": [ - { - "amount": 1, - "name": "Salad", - "price": 4.0 - } - ], - "order_total": 7.75, - "finalized": false - } + "message": "The product has been added to your order", + "order": { + "id": 1, + "products": [ + { + "amount": 1, + "name": "Salad", + "price": 4.0 + } + ], + "order_total": 7.75, + "finalized": false + } } ``` -And Finally our implementation code. + +And Finally our implementation code. First in our `addToOrder` function `this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order })` @@ -929,8 +893,9 @@ So the function looks like this ``` And in our render function + ```js -return
  • {`${item.amount} x ${item.name}`}
  • +return
  • {`${item.amount} x ${item.name}`}
  • ; ``` And finally in the return @@ -953,7 +918,7 @@ return ( ``` -commit this and lets go back to the backend for finilazing the order. +commit this and lets go back to the backend for finilazing the order. first we need to create a new request spec @@ -986,9 +951,9 @@ RSpec.describe Api::OrdersController, type: :request do end ``` -Run the specs you should and take notice of the error messages. +Run the specs you should and take notice of the error messages. -We need to add a new column in our orders database table. +We need to add a new column in our orders database table. `rails g migration AddFinalizedToOrders finalized:boolean` @@ -996,7 +961,6 @@ Before you migrate make sure that it is adding a column to the orders db table a It should look like this: - ```rb class AddFinalizedToOrders < ActiveRecord::Migration[6.0] def change @@ -1004,9 +968,10 @@ class AddFinalizedToOrders < ActiveRecord::Migration[6.0] end end ``` + run the migrations and make sure that the changes have been added to your schema file. -The final step we need to take is to refactor our update action. +The final step we need to take is to refactor our update action. `app/controllers/api/orders_controller.rb` @@ -1025,76 +990,94 @@ The final step we need to take is to refactor our update action. ``` Remember that need to update our order serializer + ```rb attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total, :finalized ``` - -Run your spec they should be green by now. +Run your spec they should be green by now. Lets head back over to the client. ## Finalizing and back to the client -As always let's start with a new spec. +As always let's start with a new spec. Add the following code to your feature test: ```js -it('user can finalize the order', () => { - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - }) - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - }) - cy.get('button').contains('View order').click() - cy.route({ - method: 'PUT', - url: 'http://localhost:3000/api/orders/1', - response: { message: 'Your order will be ready in 30 minutes!' } - }) - cy.get('button').contains('Confirm!').click() - cy.get('.message').should('contain', "Your order will be ready in 30 minutes!") -}); - +it("user can finalize the order", () => { + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("button") + .contains("View order") + .click(); + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: { message: "Your order will be ready in 30 minutes!" } + }); + cy.get("button") + .contains("Confirm!") + .click(); + cy.get(".message").should( + "contain", + "Your order will be ready in 30 minutes!" + ); +}); ``` -We also want to make sure that we have som econfigration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. +We also want to make sure that we have som econfigration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. ```js - // cypress/support/commands.js const COMMAND_DELAY = 150; - -for (const command of ['get', 'visit', 'click', 'trigger', 'type', 'clear', 'reload', 'contains']) { - Cypress.Commands.overwrite(command, (originalFn, ...args) => { - const origVal = originalFn(...args); - - return new Promise((resolve) => { - setTimeout(() => { - resolve(origVal); - }, COMMAND_DELAY); - }); +for (const command of [ + "get", + "visit", + "click", + "trigger", + "type", + "clear", + "reload", + "contains" +]) { + Cypress.Commands.overwrite(command, (originalFn, ...args) => { + const origVal = originalFn(...args); + + return new Promise(resolve => { + setTimeout(() => { + resolve(origVal); + }, COMMAND_DELAY); }); + }); } ``` Now for the implementation code: Update the state object with orderTotal + ```js state = { - productData: [], - message: {}, - orderDetails: {}, - showOrder: false, - orderTotal: '' - } + productData: [], + message: {}, + orderDetails: {}, + showOrder: false, + orderTotal: "" +}; ``` -Add a new asyncronous function to handle the finalize order +Add a new asyncronous function to handle the finalize order ```js async finalizeOrder() { @@ -1103,44 +1086,51 @@ Add a new asyncronous function to handle the finalize order this.setState({ message: { id: 0, message: result.data.message }, orderTotal: orderTotal, orderDetails: {}}) } ``` + You also need to update the addToOrder function ```js if (this.state.orderDetails.hasOwnProperty('id') && this.state.orderDetails.finalized === false) ``` -And update the return statement with the new changes. +And update the return statement with the new changes. ```js return ( - <> - {this.state.message.id === 0 && -

    {this.state.message.message}

    - } - {this.state.orderDetails.hasOwnProperty('products') && - - } - {this.state.showOrder && - <> -
      - {orderDetailsDisplay} -
    -

    To pay: {this.state.orderDetails.order_total || this.state.orderTotal} kr

    - - - } - {dataIndex} - - ) + <> + {this.state.message.id === 0 && ( +

    {this.state.message.message}

    + )} + {this.state.orderDetails.hasOwnProperty("products") && ( + + )} + {this.state.showOrder && ( + <> +
      {orderDetailsDisplay}
    +

    + To pay: {this.state.orderDetails.order_total || this.state.orderTotal}{" "} + kr +

    + + + )} + {dataIndex} + +); ``` And finally the render function: ```js -{`${item.name} ${item.description} - ${item.price}kr `} +{ + `${item.name} ${item.description} - ${item.price}kr `; +} ``` -Run your tests now and they should be green. - -We have now finalized the entire creating order functionality both in the front and the backend. +Run your tests now and they should be green. +We have now finalized the entire creating order functionality both in the front and the backend. From 3128607a1e7f520e4425b2dd4911b78106249c4e Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Tue, 7 Apr 2020 09:43:59 +0200 Subject: [PATCH 12/19] Creates files and goes through typos --- .../02_displaying_messages_and_the_backend.md | 16 +- .../03_the_orders_model.md | 59 ++++ .../react_checkout_guide/04_some_updates.md | 132 ++++++++ .../05_putting_it_togheter.md | 161 ++++++++++ .../react_checkout_guide/react-cart-final.md | 298 ------------------ 5 files changed, 360 insertions(+), 306 deletions(-) create mode 100644 guides/react_checkout_guide/03_the_orders_model.md create mode 100644 guides/react_checkout_guide/04_some_updates.md create mode 100644 guides/react_checkout_guide/05_putting_it_togheter.md diff --git a/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md b/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md index d769fba0..10a398ec 100644 --- a/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md +++ b/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md @@ -1,8 +1,7 @@ ## Displaying messages and the Backend The next thing we are going to focus on is to get the message to be displayed. - -Lets start with adding the message to the state: +Let's start with adding the message to the state: ```js state = { @@ -11,7 +10,7 @@ state = { }; ``` -Next we need to display the message below the button. +Next, we need to display the message below the button. ```js [....] @@ -19,7 +18,7 @@ Next we need to display the message below the button. {parseInt(this.state.message.id) === item.id &&

    {this.state.message.message}

    } ``` -Finally we need to set the new state in the `addToOrder` function +Finally, we need to set the new state in the `addToOrder` function ```js async addToOrder(event) { @@ -28,10 +27,9 @@ async addToOrder(event) { this.setState({message: {id: id, message: result.data.message}}) } ``` - If we run our tests now everything should go green. -Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. +Let's move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. ```bash $ touch spec/requests/api/client_can_create_new_order_spec.rb @@ -52,11 +50,11 @@ RSpec.describe Api::OrdersController, type: :request do end ``` -Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. +Remember that the request spec acts like our feature test but for APIs. This will allow us to see what requests we are sending out. Run the test and make sure that you are getting the correct error messages. -We will probably get an error message that states that we have an unitialized constant Api::OrdersController. +We will probably get an error message that states that we have an uninitialized constant `Api::OrdersController`. We will fix this by generating a new orders controller. @@ -88,3 +86,5 @@ class Api::OrdersController < ApplicationController end end ``` + +Cool. let's move over to the model. \ No newline at end of file diff --git a/guides/react_checkout_guide/03_the_orders_model.md b/guides/react_checkout_guide/03_the_orders_model.md new file mode 100644 index 00000000..5cb5d3e7 --- /dev/null +++ b/guides/react_checkout_guide/03_the_orders_model.md @@ -0,0 +1,59 @@ +## The orders model + +The next step we need to take is to generate an Order model to have something to pull out from the database. + +`$ rails generate model order` + +We also need to associate the `order` to the `order_items` that we have, and the `products`. + +`$ rails generate model OrderItems order:references product:references` + +Go ahead and go through the files that are generated and make sure that there are no problems. + +We will now add specs to test the associations we created between the models. +In the newly generated `order_items_spec.rb` go ahead and add two specs. +You will need more than that but for this guide, we will limit ourselves to these specs. + +```ruby +require 'rails_helper' + +RSpec.describe OrderItem, type: :model do + it { is_expected.to belong_to :order } + it { is_expected.to belong_to :product } +end +``` + +`order_spec.rb` + +```ruby +require 'rails_helper' + +RSpec.describe Order, type: :model do + it {is_expected.to have_many :order_items} +end +``` + +We need to make sure that the associations are present as well. + +`order_item.rb` + +```ruby +class OrderItem < ApplicationRecord + belongs_to :order + belongs_to :product +end +``` + +`order.rb` + +```ruby +class Order < ApplicationRecord + has_many :order_items +end +``` + +Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. + +![the hulk is happy](https://media.giphy.com/media/i3lbNZhnB1Jle/giphy.gif) + +Done here and working \ No newline at end of file diff --git a/guides/react_checkout_guide/04_some_updates.md b/guides/react_checkout_guide/04_some_updates.md new file mode 100644 index 00000000..840caafd --- /dev/null +++ b/guides/react_checkout_guide/04_some_updates.md @@ -0,0 +1,132 @@ +## Update the order on the Backend + +We have added a lot of functionality and e need to revisit our specs and make some updates to have it work on the client-side later on. + +First, we need to amend our request spec. + +We will amend our specs by adding a `before` block + +```ruby + before do + post '/api/orders', params: { id: product_1.id } + @order = Order.last + end +``` + +And adding a new spec + +```ruby + it 'adds another product to order if param "order_id" is present' do + post '/api/orders', params {id: product_2.id, order_id: @order.id} + expect(@order.order_items.count).to eq 2 + end +``` + +To get this to work in the first iteration we can simply refactor our create action in the controller. + +```ruby +class Api::OrdersController < ApplicationController + def create + order = if params[:order_id] + Order.find(params[:order_id]) + else + Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` + +Run your test and make sure that this is going green. It is working however this is not the best practice. Let's make sure that we follow the conventions and refactor this code so it works. Let's refactor our test first + +```ruby + it 'adds another product to order if param "order_id" is present' do + put "/api/orders/#{order.id}", params: {product_id: product_2.id } + expect(@order.order_items.count).to eq 2 + end +``` + +You will probably get a routing error. We will fix that with adding update to the routes: + +`resources :orders, only: [:create, :update]` + +Run the test again and you will get a error stating that there is no action `update`. + +```ruby + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: { message: 'The product has been added to your order'} + end +``` + +And remember to clean out the create action to its previous state: + +```ruby + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order' } + end +``` + +We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. + +```rb +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: 'Pizza') } + let!(:product_2) { create(:product, name: 'Kebab') } + + before do + post '/api/orders', params: { product_id: product_1.id } + @order_id = JSON.parse(response.body)['order_id'] + end + + describe 'POST /api/orders' do + it 'responds with success message' do + expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' + end + + it 'responds with order id' do + order = Order.find(@order_id) + expect(JSON.parse(response.body)['order_id']).to eq order.id + end + end + + describe 'PUT /api/orders/:id' do + before do + put "/api/orders/#{@order_id}", params: { product_id: product_2.id } + @order = Order.find(@order_id) + end + + it 'adds another product to order if request is a PUT and param id of the order is present' do + expect(@order.order_items.count).to eq 2 + end + + it 'responds with order id' do + expect(JSON.parse(response.body)['order_id']).to eq @order.id + end + end +end +``` + +And in the controller: + +```rb +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: { message: 'The product has been added to your order', order_id: order.id } + end + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: { message: 'The product has been added to your order', order_id: order.id } + end +end +``` \ No newline at end of file diff --git a/guides/react_checkout_guide/05_putting_it_togheter.md b/guides/react_checkout_guide/05_putting_it_togheter.md new file mode 100644 index 00000000..5789c981 --- /dev/null +++ b/guides/react_checkout_guide/05_putting_it_togheter.md @@ -0,0 +1,161 @@ +### Putting it together + +For the client to work with the backend we need to make some adjustment to the code. In your client make sure that you add product_id instead of the id. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + this.setState({message: {id: id, message: result.data.message}}) +} +``` + +## Adding multiple products to the order + +Let's add the functionality to add multiple products to our order. As always we need to have a feature test for that. We will add this to the test that we already have. +We need to do a couple of things: + +Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. + +We will also add a `PUT` request to this block in order to update the order that has already been created. And at last, we will add a test to check that we can add multiple products to an order. + +The final form of the feature will be the following: + +```js +describe("User can add a product to his/her order", () => { + beforeEach(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: { + message: "The product has been added to your order", + order_id: 1 + } + }); + + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: { + message: "The product has been added to your order", + order_id: 1 + } + }); + cy.visit("http://localhost:3001"); + }); + + it("user get a confirmation message when adding product to order", () => { + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + }); +}); +``` + +Make sure that you have added the product_id to the messages in the `beforeEach` function. + +Let's continue with the implementation code to get all of this to work. + +Add the `orderId` to the state: + +```js +state = { + productData: [], + message: {}, + orderId: "" +}; +``` + +Now we need to update the `addToOrder` function with the new `orderId` state. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) + this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) +} +``` + +The `order_id` is what is being returned to us from the backend. + +We also need to make sure that we are adding an order to an existing one, if one already exists so let's add a conditional for that. + +```js + async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderId !== "") { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + } + this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) + } +``` + +So what does this conditional do? We are simply stating that if there is an `order` and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise, go ahead and create a new order. + +Run your feature test they should no go green. Go ahead and test this manually as well. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. + +Now that we have the main functionality in place we need to start tweaking and making sure that the user experience is good. + +We want to make sure that we can view the products that we have added to our order. +First, we want to make sure that there is a button where we can view the order. +However, this button should only be visible when we have added products to our order. + +Refactor your code to look like this: + +```js + [....] + + it('user can add multiple products to order and view its content', () => { + cy.get('button').contains('View order').should('not.exist') // Add this line + cy.get('#product-2').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + + cy.get('button').contains('View order').should('exist') // Add this line + cy.get('#product-3').within(() => { + cy.get('button').contains('Add to order').click() + cy.get('.message').should('contain', "The product has been added to your order") + }) + }); +}); +``` + +And now for the implementation, let's add the button in the `DisplayProducts` component + +```js +return ( + <> + {this.state.orderId !== "" && } + {dataIndex} + +); +``` + +Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. \ No newline at end of file diff --git a/guides/react_checkout_guide/react-cart-final.md b/guides/react_checkout_guide/react-cart-final.md index 3c402263..ab29902b 100644 --- a/guides/react_checkout_guide/react-cart-final.md +++ b/guides/react_checkout_guide/react-cart-final.md @@ -200,304 +200,6 @@ Done here and working --- -## Update the order on the Backend - -First we need to amend our request spec. - -We will amend our specs by adding a before block - -```ruby - before do - post '/api/orders', params: { id: product_1.id } - @order = Order.last - end -``` - -And adding a new spec - -```ruby - it 'adds another product to order if param "order_id" is present' do - post '/api/orders', params {id: product_2.id, order_id: @order.id} - expect(@order.order_items.count).to eq 2 - end -``` - -In order to get this to work in the first iteration we can simply refactor our create action in the controller. - -```ruby -class Api::OrdersController < ApplicationController - def create - order = if params[:order_id] - Order.find(params[:order_id]) - else - Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` - -Run your test and make sure that this is going green. This is working however this is not the best practice. - -Let's make sure that we follow the conventions and refactor this code so it works. - -Lets refactor our test first - -```ruby - it 'adds another product to order if param "order_id" is present' do - put "/api/orders/#{order.id}", params: {product_id: product_2.id } - expect(@order.order_items.count).to eq 2 - end -``` - -You will probably get an routing error. We will fix that with adding update to the routes: - -`resources :orders, only: [:create, :update]` - -Run the test again and you will get a error stating that there is no action update. - -```ruby - - def update - order = Order.find(params[:id]) - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: { message: 'The product has been added to your order'} - end -``` - -And remember to clean out the create action to its previous state: - -```ruby - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order' } - end -``` - -We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. - -```rb -RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: 'Pizza') } - let!(:product_2) { create(:product, name: 'Kebab') } - - before do - post '/api/orders', params: { product_id: product_1.id } - @order_id = JSON.parse(response.body)['order_id'] - end - - describe 'POST /api/orders' do - it 'responds with success message' do - expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' - end - - it 'responds with order id' do - order = Order.find(@order_id) - expect(JSON.parse(response.body)['order_id']).to eq order.id - end - end - - describe 'PUT /api/orders/:id' do - before do - put "/api/orders/#{@order_id}", params: { product_id: product_2.id } - @order = Order.find(@order_id) - end - - it 'adds another product to order if request is a PUT and param id of the order is present' do - expect(@order.order_items.count).to eq 2 - end - - it 'responds with order id' do - expect(JSON.parse(response.body)['order_id']).to eq @order.id - end - end -end -``` - -And in the controller: - -```rb -class Api::OrdersController < ApplicationController - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end - - def update - order = Order.find(params[:id]) - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` - -### Putting it togheter - -In order for the client to work with the backend we need to make some adjustment to the code. -In your clinent make sure that you add product_id instead of the id. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - this.setState({message: {id: id, message: result.data.message}}) -} -``` - -## Adding multiple products to the order - -Let's add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. - -We need to do a couple of things: - -Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. - -We will also add a `PUT` request to this block in order to update the order that has already been created. And at last we will add a test to check that we can add multiple products to an order. - -The final form of the feature will be the following: - -```js -describe("User can add a product to his/her order", () => { - beforeEach(() => { - cy.server(); - cy.route({ - method: "GET", - url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" - }); - - cy.route({ - method: "POST", - url: "http://localhost:3000/api/orders", - response: { - message: "The product has been added to your order", - order_id: 1 - } - }); - - cy.route({ - method: "PUT", - url: "http://localhost:3000/api/orders/1", - response: { - message: "The product has been added to your order", - order_id: 1 - } - }); - cy.visit("http://localhost:3001"); - }); - - it("user get a confirmation message when adding product to order", () => { - cy.get("#product-2").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - cy.get(".message").should( - "contain", - "The product has been added to your order" - ); - }); - - cy.get("#product-3").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - cy.get(".message").should( - "contain", - "The product has been added to your order" - ); - }); - }); -}); -``` - -Make sure that you have added the product_id to the messages in the `beforeEach` function. - -Let's continue with the implementation code to get all of this to work. -add the `orderId` to the state - -```js -state = { - productData: [], - message: {}, - orderId: "" -}; -``` - -Now we need to update the `addToOrder` function with the new `orderId` state. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) - this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) -} -``` - -The `order_id` is what is being returned to us from the backend. - -We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. - -```js - async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result - if (this.state.orderId !== "") { - result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) - } else { - result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - } - this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) - } -``` - -So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. - -Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. - -Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. - -We want to make sure that we can view the products that we have added to our order. - -First we want to make sure that there is a button where we can view the order. -However this button should only be visable when we have added products to our order. - -Refactor your code to look like this: - -```js - [....] - - it('user can add multiple products to order and view its content', () => { - cy.get('button').contains('View order').should('not.exist') // Add this line - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('button').contains('View order').should('exist') // Add this line - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - }); -}); -``` - -And now for the implementation, let's add the button in the `DisplayProducts` component - -```js -return ( - <> - {this.state.orderId !== "" && } - {dataIndex} - -); -``` - -Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. We are working in very small chunks at the time ensuring that we always are making progress as we move along. From ae72d20e8d8d1ee0af0ca0c9281680922e1d527b Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Tue, 7 Apr 2020 13:55:35 +0200 Subject: [PATCH 13/19] Finializes the structure --- .../01_getting_started.md | 140 +++ .../06_viewing_the_order.md | 125 +++ .../07_some_refactoring.md | 14 + guides/react_checkout_guide/08_serializers.md | 164 ++++ .../09_back_to_the_frontend.md | 118 +++ .../10_finalizing_the_order_on_the_backend.md | 80 ++ .../11_finializing_on_the_client_side.md | 133 +++ .../react_checkout_guide/react-cart-final.md | 838 ------------------ 8 files changed, 774 insertions(+), 838 deletions(-) create mode 100644 guides/react_checkout_guide/01_getting_started.md create mode 100644 guides/react_checkout_guide/06_viewing_the_order.md create mode 100644 guides/react_checkout_guide/07_some_refactoring.md create mode 100644 guides/react_checkout_guide/08_serializers.md create mode 100644 guides/react_checkout_guide/09_back_to_the_frontend.md create mode 100644 guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md create mode 100644 guides/react_checkout_guide/11_finializing_on_the_client_side.md delete mode 100644 guides/react_checkout_guide/react-cart-final.md diff --git a/guides/react_checkout_guide/01_getting_started.md b/guides/react_checkout_guide/01_getting_started.md new file mode 100644 index 00000000..a2cf7b7f --- /dev/null +++ b/guides/react_checkout_guide/01_getting_started.md @@ -0,0 +1,140 @@ +# Adding Checkout functionality to your React application + +``` +This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. +If there are typos or issues in this guide, please notify a coach +``` + +In this section we are going to add checkout functionality to our react application. +The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. + +Start with making sure that you have the latest code pulled from GitHub on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. + +As always we will work in a test-driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. + +Create a new feature file by typing +`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` + +Add the following tests to the file: + +```js +describe("User can add a product to their order", () => { + before(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: { message: "A product has been added to your order" } + }); + }); + + it("user gests a confirmation messsage when adding a a producut to order", () => { + cy.visit("/"); + cy.get("#product-1").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.contains("A product has been added to your order"); + }); +}); +``` + +So what are we expecting to see in the implementation? Well, we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that, we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? + +When you run your test now it should all fail and to make it work we have to add the actual implementation code. + +In our case we already have a component that we use to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. + +Let's add the implementation code. + +We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are mapping over the products. + +```jsx +dataIndex = ( +
    + {this.state.productData.map(item => { + return ( +
    + {`${item.name} ${item.description} ${item.price}`} + +
    + ); + })} +
    +); +``` + +So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. + +Add the following code to the component: + +```js +addToOrder() { + debugger +} +``` + +So why are we adding a debugger here? Well, first we need to make sure that the `addToOrder` button gets invoked. + +Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome browser. + +If we can see that then its time for us to add the real functionality. + +But first let's make sure that we are hitting the right button. + +Go to your chrome console where the execution of the code has stopped and run the following command in the console: + +`event.target.parentElement` + +You should probably see something like this in your console: + +```html +
    + "Pizza" "Crusty and fluffy" "100 SEK" + +
    +``` + +If not then make sure that you are iterating over your products properly. +So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it because the values in the product are sent back to us in the form of an object. + +Add the dataset to the parent div: +`data-id={item.id} data-price={item.price}` + +Like this: + +```js +
    + {`${item.name} ${item.description} ${item.price}`} + +
    +``` + +Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. + +you should see the same information as before, but the information about the product will be returned to us in the form of an object. + +This is good news for us because we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. + +```js +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) +} +``` + +So what did we do here? First, we made the method asynchronous because we need to wait for a response from our API, we passed in the event as an argument to the function and we also added `axios` to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` + +Make sure that you add `axios` and also import it to the top of the file if you haven't already. diff --git a/guides/react_checkout_guide/06_viewing_the_order.md b/guides/react_checkout_guide/06_viewing_the_order.md new file mode 100644 index 00000000..1009c726 --- /dev/null +++ b/guides/react_checkout_guide/06_viewing_the_order.md @@ -0,0 +1,125 @@ +### Viewing the Order + +We are working in very small chunks at the time ensuring that we always are making progress as we move along. + +Now it's time for adding the view order functionality to the button. + +As always we start with the test. + +```js +cy.get("button") + .contains("View order") + .click(); +cy.get("#order-details").within(() => { + cy.get("li").should("have.length", 2); +}); + +cy.get("button") + .contains("View order") + .click(); +cy.get("#order-details").should("not.exist"); +``` + +And now for the implementation code. First we update our state object + +```js +state = { + productData: [], + message: {}, + orderId: "", + showOrder: false +}; +``` + +And then the button + +```js +return ( + <> + {this.state.orderDetails.hasOwnProperty('products') && + + } + {this.state.showOrder && +
      + {orderDetailsDisplay} +
    + } + {dataIndex} + +``` + + +Let's work on the view order + +Go to the client, first we will write the test + +```js +it("user can add multiple product to order and view its content", () => { + cy.get("button") + .contains("View order") + .should("not.exist"); + + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + + cy.get("button") + .contains("View order") + .should("exist"); + + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + + cy.get("button") + .contains("View order") + .click(); + cy.get("#order-details").within(() => { + cy.get("li").should("have.length", 2); + }); + cy.get("button") + .contains("View order") + .click(); + cy.get("#order-details").should("not.exist"); +}); +``` + +Run the test and it is failing.... +Add the following code to the return block of your component + +```js +return ( + <> + {this.state.orderId !== "" && ( + + )} + {this.state.showOrder && ( +
      +
    • Item 1
    • +
    • Item 2
    • +
    + )} + {dataIndex} + +); +``` + +Check if the tests are going green, The last one is probably not. However, we have hardcoded the values here. The best approach should be to pull in this information instead. \ No newline at end of file diff --git a/guides/react_checkout_guide/07_some_refactoring.md b/guides/react_checkout_guide/07_some_refactoring.md new file mode 100644 index 00000000..b9dfb62f --- /dev/null +++ b/guides/react_checkout_guide/07_some_refactoring.md @@ -0,0 +1,14 @@ + +Some quick refactoring + +Lets go back to our test and refactor them a little + +Add the fixture files to the test + +```bash +$ touch cypress/fixtures/put_response.json +$ touch cypress/fixtures/post_response.json +``` + +zap i should have committed a loong time ago +(update this with the code from the client) \ No newline at end of file diff --git a/guides/react_checkout_guide/08_serializers.md b/guides/react_checkout_guide/08_serializers.md new file mode 100644 index 00000000..94cfa0e6 --- /dev/null +++ b/guides/react_checkout_guide/08_serializers.md @@ -0,0 +1,164 @@ +### Serializers + +Lets move go back to our backend to continue this functionality + +We want to add a gem called Active Model Serializer +So what is a serializer? A serializer makes sure that our json responses has particular shape and content, rather than the content provided by the built in json responses. We want to be in control of the information that is leaving the application, and in order to do that we are using this gem. + +Add the gem to your `Gemfile` and run bundle + +```rb +gem 'active_model_serializers' +``` + +Create the file a configuration file where we are adding the settings. + +```bash +$ touch config/initializers/active_model_serializers.rb +``` + +Add + +```rb +ActiveModelSerializers.config.adapter = :json +``` + +Now lets generate a new serializer. In the future a new serializer will be generated by default if you generate a new model. But for our existing model we will run the generator. + +```bash +$ rails g serializer order +``` + +This generator creates a new file. But before we add code to this (and there is a lot of code to add) we need to refactor our test and implementation to match the serialization. + +Go over the following code carefully and take note of the changes we are making. + +`spec/requests/api/client_can_create_new_order_spec.rb` + +```rb +RSpec.describe Api::OrdersController, type: :request do + let!(:product_1) { create(:product, name: "Pizza", price: 10) } + let!(:product_2) { create(:product, name: "Kebab", price: 20) } + + before do + post "/api/orders", params: { product_id: product_1.id } + order_id = JSON.parse(response.body)["order"]["id"] + @order = Order.find(order_id) + end + + describe "POST /api/orders" do + it "responds with success message" do + expect(JSON.parse(response.body)["message"]).to eq "The product has been added to your order" + end + + it "responds with order id" do + expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id + end + + it "responds with right amount of products" do + expect(JSON.parse(response.body)["order"]["products"].count).to eq 1 + end + + it "responds with right order total" do + expect(JSON.parse(response.body)["order"]["total"]).to eq 10 + end + end + + describe "PUT /api/orders/:id" do + before do + put "/api/orders/#{@order.id}", params: { product_id: product_2.id } + put "/api/orders/#{@order.id}", params: { product_id: product_2.id } + end + + it "adds another product to order if request is a PUT and param id of the order is present" do + expect(@order.order_items.count).to eq 3 + end + + it "responds with order id" do + expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id + end + + it "responds with right amount of unique products" do + expect(JSON.parse(response.body)["order"]["products"].count).to eq 2 + end + + it "responds with right order total" do + expect(JSON.parse(response.body)["order"]["total"]).to eq 50 + end + end +end +``` + +And now for the serializer + +`app/serializers/order_serializer.rb` + +```rb +class OrderSerializer < ActiveModel::Serializer + attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total + + def products_1 + products = [] + object.order_items.group_by(&:product_id).each do |key, value| + product = Product.find(key) + hash = { amount: value.count, name: product.name, total: (product.price * value.count) } + products.push hash + end + products + end + + def products_2 + products = [] + unique_items = object.order_items.uniq(&:product) + unique_items_count = object.order_items.group_by(&:product_id).map { |key, value| [key, value.size] }.to_h + unique_items.each do |item| + products.push( + amount: unique_items_count[item.product_id], + name: item.product.name, + total: (unique_items_count[item.product_id] * item.product.price) + ) + end + products + end + + def products_3 + object.order_items.group_by(&:product_id).map do |_key, value| + product = value.uniq(&:product_id)[0].product + { amount: value.size, name: product.name, total: (value.size * product.price) } + end + end + alias_method :products, :products_3 + + def total + object.order_items.joins(:product).sum('products.price') + end +end +``` + +And finally we need to update our controller with the changes. + +`app/controllers/api/orders_controller.rb` + +```rb +class Api::OrdersController < ApplicationController + def create + order = Order.create + order.order_items.create(product_id: params[:product_id]) + render json: create_json_response(order) + end + + def update + order = Order.find(params[:id]) + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) + end + +private + + def create_json_response(order) + json = { order: OrderSerializer.new(order) } + json.merge!(message: 'The product has been added to your order') + end +end +``` diff --git a/guides/react_checkout_guide/09_back_to_the_frontend.md b/guides/react_checkout_guide/09_back_to_the_frontend.md new file mode 100644 index 00000000..365cd962 --- /dev/null +++ b/guides/react_checkout_guide/09_back_to_the_frontend.md @@ -0,0 +1,118 @@ +## Go back to the frontend + +Now we want to go back to our frontend and add the changes there as well. We need to make sure that the order totals are showing up. + +As always we first start with our tests. + +```js +cy.get("button") + .contains("View order") + .click(); +cy.get("#order-details").within(() => { + cy.get("li") + .should("have.length", 2) + .first() + .should("have.text", "1 x Salad") + .next() + .should("have.text", "1 x Ice Cream"); +}); +``` + +We also need to import the DisplayProductData to the feature file. + +`import DisplayProductData from '../../src/components/DisplayProductData'` + +And of course we need to update our fixture files to accomodate the new changes + +```json +// put_response.json + +{ + "message": "The product has been added to your order", + "order": { + "id": 1, + "products": [ + { + "amount": 1, + "name": "Salad", + "price": 4.0 + }, + { + "amount": 1, + "name": "Ice Cream", + "price": 3.75 + } + ], + "order_total": 7.75, + "finalized": false + } +} +``` + +and + +```json +// post_response.json +{ + "message": "The product has been added to your order", + "order": { + "id": 1, + "products": [ + { + "amount": 1, + "name": "Salad", + "price": 4.0 + } + ], + "order_total": 7.75, + "finalized": false + } +} +``` + +And Finally our implementation code. + +First in our `addToOrder` function +`this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order })` + +So the function looks like this + +```js + async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderDetails.hasOwnProperty('id')) { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id }) + } + this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order }) +``` + +And in our render function + +```js +return
  • {`${item.amount} x ${item.name}`}
  • ; +``` + +And finally in the return + +```js +return ( + <> + {this.state.orderDetails.hasOwnProperty('products') && + + } + {this.state.showOrder && + <> +
      + {orderDetailsDisplay} +
    +

    To pay: {this.state.orderDetails.order_total}

    + + } + {dataIndex} + +``` + +commit this and lets go back to the backend for finilazing the order. \ No newline at end of file diff --git a/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md b/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md new file mode 100644 index 00000000..1d6294a5 --- /dev/null +++ b/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md @@ -0,0 +1,80 @@ +## Finalizing the order + +first we need to create a new request spec + +`$ touch spec/requests/api/user_can_finalize_order_spec.rb` + +inside of this file add the followign specs + +```rb +RSpec.describe Api::OrdersController, type: :request do + let(:product_1) { create(:product, name: 'Pizza', price: 10) } + let(:product_2) { create(:product, name: 'Kebab', price: 20) } + let(:order) { create(:order) } + + before do + order.order_items.create(product: product_1) + order.order_items.create(product: product_2) + + put "/api/orders/#{order.id}", params: { activity: 'finalize' } + end + + describe 'PUT /api/orders' do + it 'responds with success message' do + expect(JSON.parse(response.body)['message']).to eq 'Your order will be ready in 30 minutes!' + end + + it 'sets the order attribute "finalized" to true' do + expect(order.reload.finalized).to eq true + end + end +end +``` + +Run the specs you should and take notice of the error messages. + +We need to add a new column in our orders database table. + +`rails g migration AddFinalizedToOrders finalized:boolean` + +Before you migrate make sure that it is adding a column to the orders db table and make sure that the default value is set to false. + +It should look like this: + +```rb +class AddFinalizedToOrders < ActiveRecord::Migration[6.0] + def change + add_column :orders, :finalized, :boolean, default: false + end +end +``` + +run the migrations and make sure that the changes have been added to your schema file. + +The final step we need to take is to refactor our update action. + +`app/controllers/api/orders_controller.rb` + +```rb + def update + order = Order.find(params[:id]) + if params[:activity] + order.update_attribute(:finalized, true) + render json: { message: 'Your order will be ready in 30 minutes!' } + else + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) + end + end +``` + +Remember that need to update our order serializer + +```rb +attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total, :finalized +``` + +Run your spec they should be green by now. + +Lets head back over to the client. \ No newline at end of file diff --git a/guides/react_checkout_guide/11_finializing_on_the_client_side.md b/guides/react_checkout_guide/11_finializing_on_the_client_side.md new file mode 100644 index 00000000..e30b0117 --- /dev/null +++ b/guides/react_checkout_guide/11_finializing_on_the_client_side.md @@ -0,0 +1,133 @@ +## Finalizing and back to the client + +As always let's start with a new spec. + +Add the following code to your feature test: + +```js +it("user can finalize the order", () => { + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("button") + .contains("View order") + .click(); + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: { message: "Your order will be ready in 30 minutes!" } + }); + cy.get("button") + .contains("Confirm!") + .click(); + cy.get(".message").should( + "contain", + "Your order will be ready in 30 minutes!" + ); +}); +``` + +We also want to make sure that we have som configration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. + +```js +// cypress/support/commands.js +const COMMAND_DELAY = 150; + +for (const command of [ + "get", + "visit", + "click", + "trigger", + "type", + "clear", + "reload", + "contains" +]) { + Cypress.Commands.overwrite(command, (originalFn, ...args) => { + const origVal = originalFn(...args); + + return new Promise(resolve => { + setTimeout(() => { + resolve(origVal); + }, COMMAND_DELAY); + }); + }); +} +``` + +Now for the implementation code: + +Update the state object with orderTotal + +```js +state = { + productData: [], + message: {}, + orderDetails: {}, + showOrder: false, + orderTotal: "" +}; +``` + +Add a new asyncronous function to handle the finalize order + +```js + async finalizeOrder() { + let orderTotal = this.state.orderDetails.order_total + let result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { activity: 'finalize' }) + this.setState({ message: { id: 0, message: result.data.message }, orderTotal: orderTotal, orderDetails: {}}) + } +``` + +You also need to update the addToOrder function + +```js +if (this.state.orderDetails.hasOwnProperty('id') && this.state.orderDetails.finalized === false) +``` + +And update the return statement with the new changes. + +```js +return ( + <> + {this.state.message.id === 0 && ( +

    {this.state.message.message}

    + )} + {this.state.orderDetails.hasOwnProperty("products") && ( + + )} + {this.state.showOrder && ( + <> +
      {orderDetailsDisplay}
    +

    + To pay: {this.state.orderDetails.order_total || this.state.orderTotal}{" "} + kr +

    + + + )} + {dataIndex} + +); +``` + +And finally the render function: + +```js +{ + `${item.name} ${item.description} - ${item.price}kr `; +} +``` + +Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. \ No newline at end of file diff --git a/guides/react_checkout_guide/react-cart-final.md b/guides/react_checkout_guide/react-cart-final.md deleted file mode 100644 index ab29902b..00000000 --- a/guides/react_checkout_guide/react-cart-final.md +++ /dev/null @@ -1,838 +0,0 @@ -# Adding Checkout functionality to your React application - -``` -This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. -If there are typos or issues in this guide, please notify a coach -``` - -In this section we are going to add checkout functionality to our react application. -The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. - -Start with making sure that you have the latest code pulled from GitHub on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. - -As always we will work in a test-driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. - -Create a new feature file by typing -`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` - -Add the following tests to the file: - -```js -describe("User can add a product to their order", () => { - before(() => { - cy.server(); - cy.route({ - method: "GET", - url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" - }); - - cy.route({ - method: "POST", - url: "http://localhost:3000/api/orders", - response: { message: "A product has been added to your order" } - }); - }); - - it("user gests a confirmation messsage when adding a a producut to order", () => { - cy.visit("/"); - cy.get("#product-1").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - }); - cy.contains("A product has been added to your order"); - }); -}); -``` - -So what are we expecting to see in the implementation? Well, we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that, we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? - -When you run your test now it should all fail and to make it work we have to add the actual implementation code. - -In our case we already have a component that we use to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. - -Let's add the implementation code. - -We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are mapping over the products. - -```jsx -dataIndex = ( -
    - {this.state.productData.map(item => { - return ( -
    - {`${item.name} ${item.description} ${item.price}`} - -
    - ); - })} -
    -); -``` - -So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. - -Add the following code to the component: - -```js -addToOrder() { - debugger -} -``` - -So why are we adding a debugger here? Well, first we need to make sure that the `addToOrder` button gets invoked. - -Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome browser. - -If we can see that then its time for us to add the real functionality. - -But first let's make sure that we are hitting the right button. - -Go to your chrome console where the execution of the code has stopped and run the following command in the console: - -`event.target.parentElement` - -You should probably see something like this in your console: - -```html -
    - "Pizza" "Crusty and fluffy" "100 SEK" - -
    -``` - -If not then make sure that you are iterating over your products properly. -So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it because the values in the product are sent back to us in the form of an object. - -Add the dataset to the parent div: -`data-id={item.id} data-price={item.price}` - -Like this: - -```js -
    - {`${item.name} ${item.description} ${item.price}`} - -
    -``` - -Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. - -you should see the same information as before, but the information about the product will be returned to us in the form of an object. - -This is good news for us because we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) -} -``` - -So what did we do here? First, we made the method asynchronous because we need to wait for a response from our API, we passed in the event as an argument to the function and we also added `axios` to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` - -Make sure that you add `axios` and also import it to the top of the file if you haven't already. - ----------------------------- - - -The next step we need to take is to generate a Order model to have something to pull out from the database. - -`$ rails generate model order` - -We also need to associate the the order to the order items that we have, and the products. - -`$ rails generate model OrderItems order:references product:references` - -Go ahead and go through the files that are generated and make sure that there are no problems. - -We will now add specs to test the associations we created between the models. -In the newly generated `order_items_spec.rb` go ahead and add two specs. -You will need more than that but for this guide we will limit ourselves to these specs. - -```ruby -require 'rails_helper' - -RSpec.describe OrderItem, type: :model do - it { is_expected.to belong_to :order } - it { is_expected.to belong_to :product } -end -``` - -`order_spec.rb` - -```ruby -require 'rails_helper' - -RSpec.describe Order, type: :model do - it {is_expected.to have_many :order_items} -end -``` - -We need to make sure that the associations are present as well. - -`order_item.rb` - -```ruby -class OrderItem < ApplicationRecord - belongs_to :order - belongs_to :product -end -``` - -`order.rb` - -```ruby -class Order < ApplicationRecord - has_many :order_items -end -``` - -Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. - -Done here and working - ---- - - -We are working in very small chunks at the time ensuring that we always are making progress as we move along. - -Now it's time for adding the view order functionality to the button. - -As always we start with the test. - -```js -cy.get("button") - .contains("View order") - .click(); -cy.get("#order-details").within(() => { - cy.get("li").should("have.length", 2); -}); - -cy.get("button") - .contains("View order") - .click(); -cy.get("#order-details").should("not.exist"); -``` - -And ow for the implementation code. First we update our state object - -```js -state = { - productData: [], - message: {}, - orderId: "", - showOrder: false -}; -``` - -And then the button - -```js -return ( - <> - {this.state.orderDetails.hasOwnProperty('products') && - - } - {this.state.showOrder && -
      - {orderDetailsDisplay} -
    - } - {dataIndex} - -``` - -## Viewing the order - -Let's work on the view order - -Go to the client, first we willl write the test - -```js -it("user can add multiple product to order and view its content", () => { - cy.get("button") - .contains("View order") - .should("not.exist"); - - cy.get("#product-2").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - cy.get(".message").should( - "contain", - "The product has been added to your order" - ); - }); - - cy.get("button") - .contains("View order") - .should("exist"); - - cy.get("#product-3").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - cy.get(".message").should( - "contain", - "The product has been added to your order" - ); - }); - - cy.get("button") - .contains("View order") - .click(); - cy.get("#order-details").within(() => { - cy.get("li").should("have.length", 2); - }); - cy.get("button") - .contains("View order") - .click(); - cy.get("#order-details").should("not.exist"); -}); -``` - -Run the test and it is failing.... -Add the following code to the return block of your component - -```js -return ( - <> - {this.state.orderId !== "" && ( - - )} - {this.state.showOrder && ( -
      -
    • Item 1
    • -
    • Item 2
    • -
    - )} - {dataIndex} - -); -``` - -Check if the tests are going green, The last one is probably not. However we have hard coded the values here. The best approach should be to pull in this information instead. - -Lets go back to our test and refactor them a little - -Add the fixture files to the test - -```bash -$ touch cypress/fixtures/put_response.json -$ touch cypress/fixtures/post_response.json -``` - -zap i should have committed a loong time ago -(update this with the code from the client) - -### Serializers - -Lets move go back to our backend to continue this functionality - -We want to add a gem called Active Model Serializer -So what is a serializer? A serializer makes sure that our json responses has particular shape and content, rather than the content provided by the built in json responses. We want to be in control of the information that is leaving the application, and in order to do that we are using this gem. - -Add the gem to your `Gemfile` and run bundle - -```rb -gem 'active_model_serializers' -``` - -Create the file a configuration file where we are adding the settings. - -```bash -$ touch config/initializers/active_model_serializers.rb -``` - -Add - -```rb -ActiveModelSerializers.config.adapter = :json -``` - -Now lets generate a new serializer. In the future a new serializer will be generated by default if you generate a new model. But for our existing model we will run the generator. - -```bash -$ rails g serializer order -``` - -This generator creates a new file. But before we add code to this (and there is a lot of code to add) we need to refactor our test and implementation to match the serialization. - -Go over the following code carefully and take note of the changes we are making. - -`spec/requests/api/client_can_create_new_order_spec.rb` - -```rb -RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: "Pizza", price: 10) } - let!(:product_2) { create(:product, name: "Kebab", price: 20) } - - before do - post "/api/orders", params: { product_id: product_1.id } - order_id = JSON.parse(response.body)["order"]["id"] - @order = Order.find(order_id) - end - - describe "POST /api/orders" do - it "responds with success message" do - expect(JSON.parse(response.body)["message"]).to eq "The product has been added to your order" - end - - it "responds with order id" do - expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id - end - - it "responds with right amount of products" do - expect(JSON.parse(response.body)["order"]["products"].count).to eq 1 - end - - it "responds with right order total" do - expect(JSON.parse(response.body)["order"]["total"]).to eq 10 - end - end - - describe "PUT /api/orders/:id" do - before do - put "/api/orders/#{@order.id}", params: { product_id: product_2.id } - put "/api/orders/#{@order.id}", params: { product_id: product_2.id } - end - - it "adds another product to order if request is a PUT and param id of the order is present" do - expect(@order.order_items.count).to eq 3 - end - - it "responds with order id" do - expect(JSON.parse(response.body)["order"]["id"]).to eq @order.id - end - - it "responds with right amount of unique products" do - expect(JSON.parse(response.body)["order"]["products"].count).to eq 2 - end - - it "responds with right order total" do - expect(JSON.parse(response.body)["order"]["total"]).to eq 50 - end - end -end -``` - -And now for the serializer - -`app/serializers/order_serializer.rb` - -```rb -class OrderSerializer < ActiveModel::Serializer - attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total - - def products_1 - products = [] - object.order_items.group_by(&:product_id).each do |key, value| - product = Product.find(key) - hash = { amount: value.count, name: product.name, total: (product.price * value.count) } - products.push hash - end - products - end - - def products_2 - products = [] - unique_items = object.order_items.uniq(&:product) - unique_items_count = object.order_items.group_by(&:product_id).map { |key, value| [key, value.size] }.to_h - unique_items.each do |item| - products.push( - amount: unique_items_count[item.product_id], - name: item.product.name, - total: (unique_items_count[item.product_id] * item.product.price) - ) - end - products - end - - def products_3 - object.order_items.group_by(&:product_id).map do |_key, value| - product = value.uniq(&:product_id)[0].product - { amount: value.size, name: product.name, total: (value.size * product.price) } - end - end - alias_method :products, :products_3 - - def total - object.order_items.joins(:product).sum('products.price') - end -end -``` - -And finally we need to update our controller with the changes. - -`app/controllers/api/orders_controller.rb` - -```rb -class Api::OrdersController < ApplicationController - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: create_json_response(order) - end - - def update - order = Order.find(params[:id]) - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: create_json_response(order) - end - -private - - def create_json_response(order) - json = { order: OrderSerializer.new(order) } - json.merge!(message: 'The product has been added to your order') - end -end -``` - -## Go back to the frontend - -Now we want to go back to our frontend and add the changes there as well. We need to make sure that the order totals are showing up. - -As always we first start with our tests. - -```js -cy.get("button") - .contains("View order") - .click(); -cy.get("#order-details").within(() => { - cy.get("li") - .should("have.length", 2) - .first() - .should("have.text", "1 x Salad") - .next() - .should("have.text", "1 x Ice Cream"); -}); -``` - -We also need to import the DisplayProductData to the feature file. - -`import DisplayProductData from '../../src/components/DisplayProductData'` - -And of course we need to update our fixture files to accomodate the new changes - -```json -// put_response.json - -{ - "message": "The product has been added to your order", - "order": { - "id": 1, - "products": [ - { - "amount": 1, - "name": "Salad", - "price": 4.0 - }, - { - "amount": 1, - "name": "Ice Cream", - "price": 3.75 - } - ], - "order_total": 7.75, - "finalized": false - } -} -``` - -and - -```json -// post_response.json -{ - "message": "The product has been added to your order", - "order": { - "id": 1, - "products": [ - { - "amount": 1, - "name": "Salad", - "price": 4.0 - } - ], - "order_total": 7.75, - "finalized": false - } -} -``` - -And Finally our implementation code. - -First in our `addToOrder` function -`this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order })` - -So the function looks like this - -```js - async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result - if (this.state.orderDetails.hasOwnProperty('id')) { - result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { product_id: id }) - } else { - result = await axios.post('http://localhost:3000/api/orders', { product_id: id }) - } - this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order }) -``` - -And in our render function - -```js -return
  • {`${item.amount} x ${item.name}`}
  • ; -``` - -And finally in the return - -```js -return ( - <> - {this.state.orderDetails.hasOwnProperty('products') && - - } - {this.state.showOrder && - <> -
      - {orderDetailsDisplay} -
    -

    To pay: {this.state.orderDetails.order_total}

    - - } - {dataIndex} - -``` - -commit this and lets go back to the backend for finilazing the order. - -first we need to create a new request spec - -`$ touch spec/requests/api/user_can_finalize_order_spec.rb` - -inside of this file add the followign specs - -```rb -RSpec.describe Api::OrdersController, type: :request do - let(:product_1) { create(:product, name: 'Pizza', price: 10) } - let(:product_2) { create(:product, name: 'Kebab', price: 20) } - let(:order) { create(:order) } - - before do - order.order_items.create(product: product_1) - order.order_items.create(product: product_2) - - put "/api/orders/#{order.id}", params: { activity: 'finalize' } - end - - describe 'PUT /api/orders' do - it 'responds with success message' do - expect(JSON.parse(response.body)['message']).to eq 'Your order will be ready in 30 minutes!' - end - - it 'sets the order attribute "finalized" to true' do - expect(order.reload.finalized).to eq true - end - end -end -``` - -Run the specs you should and take notice of the error messages. - -We need to add a new column in our orders database table. - -`rails g migration AddFinalizedToOrders finalized:boolean` - -Before you migrate make sure that it is adding a column to the orders db table and make sure that the default value is set to false. - -It should look like this: - -```rb -class AddFinalizedToOrders < ActiveRecord::Migration[6.0] - def change - add_column :orders, :finalized, :boolean, default: false - end -end -``` - -run the migrations and make sure that the changes have been added to your schema file. - -The final step we need to take is to refactor our update action. - -`app/controllers/api/orders_controller.rb` - -```rb - def update - order = Order.find(params[:id]) - if params[:activity] - order.update_attribute(:finalized, true) - render json: { message: 'Your order will be ready in 30 minutes!' } - else - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: create_json_response(order) - end - end -``` - -Remember that need to update our order serializer - -```rb -attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total, :finalized -``` - -Run your spec they should be green by now. - -Lets head back over to the client. - -## Finalizing and back to the client - -As always let's start with a new spec. - -Add the following code to your feature test: - -```js -it("user can finalize the order", () => { - cy.get("#product-2").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - }); - cy.get("#product-3").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - }); - cy.get("button") - .contains("View order") - .click(); - cy.route({ - method: "PUT", - url: "http://localhost:3000/api/orders/1", - response: { message: "Your order will be ready in 30 minutes!" } - }); - cy.get("button") - .contains("Confirm!") - .click(); - cy.get(".message").should( - "contain", - "Your order will be ready in 30 minutes!" - ); -}); -``` - -We also want to make sure that we have som econfigration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. - -```js -// cypress/support/commands.js -const COMMAND_DELAY = 150; - -for (const command of [ - "get", - "visit", - "click", - "trigger", - "type", - "clear", - "reload", - "contains" -]) { - Cypress.Commands.overwrite(command, (originalFn, ...args) => { - const origVal = originalFn(...args); - - return new Promise(resolve => { - setTimeout(() => { - resolve(origVal); - }, COMMAND_DELAY); - }); - }); -} -``` - -Now for the implementation code: - -Update the state object with orderTotal - -```js -state = { - productData: [], - message: {}, - orderDetails: {}, - showOrder: false, - orderTotal: "" -}; -``` - -Add a new asyncronous function to handle the finalize order - -```js - async finalizeOrder() { - let orderTotal = this.state.orderDetails.order_total - let result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { activity: 'finalize' }) - this.setState({ message: { id: 0, message: result.data.message }, orderTotal: orderTotal, orderDetails: {}}) - } -``` - -You also need to update the addToOrder function - -```js -if (this.state.orderDetails.hasOwnProperty('id') && this.state.orderDetails.finalized === false) -``` - -And update the return statement with the new changes. - -```js -return ( - <> - {this.state.message.id === 0 && ( -

    {this.state.message.message}

    - )} - {this.state.orderDetails.hasOwnProperty("products") && ( - - )} - {this.state.showOrder && ( - <> -
      {orderDetailsDisplay}
    -

    - To pay: {this.state.orderDetails.order_total || this.state.orderTotal}{" "} - kr -

    - - - )} - {dataIndex} - -); -``` - -And finally the render function: - -```js -{ - `${item.name} ${item.description} - ${item.price}kr `; -} -``` - -Run your tests now and they should be green. - -We have now finalized the entire creating order functionality both in the front and the backend. From c8e156cd3ebffcd4c7d5e99b5048b05aefd7dbf2 Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Tue, 7 Apr 2020 14:21:11 +0200 Subject: [PATCH 14/19] goes through all typos --- .../react_checkout_guide/01_getting_started.md | 5 +---- guides/react_checkout_guide/08_serializers.md | 11 +++++++---- .../09_back_to_the_frontend.md | 8 ++++---- .../10_finalizing_the_order_on_the_backend.md | 17 +++++++---------- .../11_finializing_on_the_client_side.md | 10 +++++----- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/guides/react_checkout_guide/01_getting_started.md b/guides/react_checkout_guide/01_getting_started.md index a2cf7b7f..18cfc8ad 100644 --- a/guides/react_checkout_guide/01_getting_started.md +++ b/guides/react_checkout_guide/01_getting_started.md @@ -123,10 +123,7 @@ Like this: ``` Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. - -you should see the same information as before, but the information about the product will be returned to us in the form of an object. - -This is good news for us because we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. +You should see the same information as before, but the information about the product will be returned to us in the form of an object. This is good news for us because we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. ```js async addToOrder(event) { diff --git a/guides/react_checkout_guide/08_serializers.md b/guides/react_checkout_guide/08_serializers.md index 94cfa0e6..087ce92a 100644 --- a/guides/react_checkout_guide/08_serializers.md +++ b/guides/react_checkout_guide/08_serializers.md @@ -1,9 +1,10 @@ ### Serializers -Lets move go back to our backend to continue this functionality +Let's move to our backend to continue this functionality We want to add a gem called Active Model Serializer -So what is a serializer? A serializer makes sure that our json responses has particular shape and content, rather than the content provided by the built in json responses. We want to be in control of the information that is leaving the application, and in order to do that we are using this gem. + +So what is a serializer? A serializer makes sure that our JSON responses has particular shape and content, rather than the content provided by the built-in JSON responses. We want to be in control of the information that is leaving the application, and to do that we are using this gem. Add the gem to your `Gemfile` and run bundle @@ -23,14 +24,16 @@ Add ActiveModelSerializers.config.adapter = :json ``` -Now lets generate a new serializer. In the future a new serializer will be generated by default if you generate a new model. But for our existing model we will run the generator. +Now let's generate a new serializer. +In the future a new serializer will be generated by default if you generate a new model. +But for our existing model, we will run the generator. + ```bash $ rails g serializer order ``` This generator creates a new file. But before we add code to this (and there is a lot of code to add) we need to refactor our test and implementation to match the serialization. - Go over the following code carefully and take note of the changes we are making. `spec/requests/api/client_can_create_new_order_spec.rb` diff --git a/guides/react_checkout_guide/09_back_to_the_frontend.md b/guides/react_checkout_guide/09_back_to_the_frontend.md index 365cd962..1fd922a8 100644 --- a/guides/react_checkout_guide/09_back_to_the_frontend.md +++ b/guides/react_checkout_guide/09_back_to_the_frontend.md @@ -18,11 +18,11 @@ cy.get("#order-details").within(() => { }); ``` -We also need to import the DisplayProductData to the feature file. +We also need to import the `DisplayProductData` to the feature file. `import DisplayProductData from '../../src/components/DisplayProductData'` -And of course we need to update our fixture files to accomodate the new changes +And of course, we need to update our fixture files to accommodate the new changes ```json // put_response.json @@ -70,7 +70,7 @@ and } ``` -And Finally our implementation code. +And finally our implementation code. First in our `addToOrder` function `this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order })` @@ -115,4 +115,4 @@ return ( ``` -commit this and lets go back to the backend for finilazing the order. \ No newline at end of file +Commit this and let's go back to the backend to finalize the order. \ No newline at end of file diff --git a/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md b/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md index 1d6294a5..27e3b058 100644 --- a/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md +++ b/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md @@ -1,10 +1,11 @@ ## Finalizing the order -first we need to create a new request spec +We will start by adding the functionality to the backend. +First, we need to create a new request spec `$ touch spec/requests/api/user_can_finalize_order_spec.rb` -inside of this file add the followign specs +Inside of this file add the following specs ```rb RSpec.describe Api::OrdersController, type: :request do @@ -32,13 +33,11 @@ end ``` Run the specs you should and take notice of the error messages. - We need to add a new column in our orders database table. `rails g migration AddFinalizedToOrders finalized:boolean` -Before you migrate make sure that it is adding a column to the orders db table and make sure that the default value is set to false. - +Before you migrate make sure that it is adding a column to the orders db:table and make sure that the default value is set to false. It should look like this: ```rb @@ -49,9 +48,7 @@ class AddFinalizedToOrders < ActiveRecord::Migration[6.0] end ``` -run the migrations and make sure that the changes have been added to your schema file. - -The final step we need to take is to refactor our update action. +Run the migrations and make sure that the changes have been added to your schema file. The final step we need to take is to refactor our update action. `app/controllers/api/orders_controller.rb` @@ -69,7 +66,7 @@ The final step we need to take is to refactor our update action. end ``` -Remember that need to update our order serializer +Remember that need to update our order serializer as well. ```rb attributes :id, :products_1, :products_2, :products_3, :products, :total, :order_total, :finalized @@ -77,4 +74,4 @@ attributes :id, :products_1, :products_2, :products_3, :products, :total, :order Run your spec they should be green by now. -Lets head back over to the client. \ No newline at end of file +Let's head back over to the client. \ No newline at end of file diff --git a/guides/react_checkout_guide/11_finializing_on_the_client_side.md b/guides/react_checkout_guide/11_finializing_on_the_client_side.md index e30b0117..73aa7c85 100644 --- a/guides/react_checkout_guide/11_finializing_on_the_client_side.md +++ b/guides/react_checkout_guide/11_finializing_on_the_client_side.md @@ -1,5 +1,6 @@ ## Finalizing and back to the client +Okay...The final stretch As always let's start with a new spec. Add the following code to your feature test: @@ -34,8 +35,7 @@ it("user can finalize the order", () => { }); ``` -We also want to make sure that we have som configration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. - +We also want to make sure that we have som configuration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. ```js // cypress/support/commands.js const COMMAND_DELAY = 150; @@ -76,7 +76,7 @@ state = { }; ``` -Add a new asyncronous function to handle the finalize order +Add a new asynchronous function to handle the finalize order ```js async finalizeOrder() { @@ -86,7 +86,7 @@ Add a new asyncronous function to handle the finalize order } ``` -You also need to update the addToOrder function +You also need to update the `addToOrder` function ```js if (this.state.orderDetails.hasOwnProperty('id') && this.state.orderDetails.finalized === false) @@ -130,4 +130,4 @@ And finally the render function: } ``` -Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. \ No newline at end of file +Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. Go over your code again and marvel at your creation. This is one approach we can take to create an ordering functionality in React from scratch. \ No newline at end of file From 57925844eb170b368f3065b35a6eee62ff91baac Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Tue, 7 Apr 2020 14:45:39 +0200 Subject: [PATCH 15/19] Adds missing section and deletes old files --- cart_to_react.md | 447 ------------ .../07_some_refactoring.md | 214 +++++- react-cart-updated.md | 646 ------------------ 3 files changed, 209 insertions(+), 1098 deletions(-) delete mode 100644 cart_to_react.md delete mode 100644 react-cart-updated.md diff --git a/cart_to_react.md b/cart_to_react.md deleted file mode 100644 index c187c968..00000000 --- a/cart_to_react.md +++ /dev/null @@ -1,447 +0,0 @@ -## Adding cart functionality to React - -In this section we are going to add checkout functionality to our react application. -The prerequsitet for this functionality is that we have products on our page that are ready to sell. - -Start with making sure that you have the latest code pulled from github on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. - -As always we will work in a test driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. - - -Create a new feature file by typing -`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` - -Add the following tests to the file: - -```js -describe("User can add a product to their order", () => { - before(() => { - cy.server(); - cy.route({ - method: "GET", - url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" - }); - - cy.route({ - method: "POST", - url: "http://localhost:3000/api/orders", - response: { message: "A product has been added to your order" } - }); - }); - - it("user gests a confirmation messsage when adding a a producut to order", () => { - cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API - cy.get("#product-1").within(() => { - cy.get("#button") - .contains("Add to order") - .click(); - }); - cy.wait(3000) - }); -}); -``` -So what are we excpecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? - -When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. - -In our case we already have a component that we use in order to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. - -Let's add the implementation code. - -We need to start with the first step which is to make sure that we have a button available to click on, let's add that. - -```jsx -
    - {`${item.name} ${item.description} ${item.price}`} - -
    -``` -So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. - -Add the following code to the component: - -```js -addToOrder() { - debugger -} -``` - -So why are we adding a debugger here? Well first we need to make sure that the `addToOrder` button actually gets invoked. - -Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome broweser. - -If we can see that then its time for us to add the real functionality. -But first lets make sure that we are hitting the right button. -Go to your chrome console where the execution of the code has stopped and run the following command in the console: - -`event.target.parentElement` - -You should probably see something like this in your console: - -```html -
    - "Pizza" - "Crusty and fluffy" - "100 SEK" - -
    -``` -If not then make sure that you are iterating over your products properly. - -So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it beacuse the values in the product are sent back to us in the form of an object. - -Add the dataset to the parent div: - -```js -
    - {`${item.name} ${item.description} ${item.price}`} - -
    -``` - -Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. -you should see the same information as before, but the information about the product will be returend to us in the form of an object. - -This is good new for us beacuse we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) -} -``` - -So what did we do here? First we made the method asyncronous beacuse we need to wait for a response from our API, we passed in the event as an argument to the function and we also added axios in order to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` - -Make sure that you add axios and also import it to the top of the file if you haven't already. - -The next thing we are going to focus on is to get the message to be displayed. - -Lets start with adding the message to the state: - -```js -state = { - productData: [], - message: {} - } -``` - -Next we need to display the message below the button. - -```js -[....] - - {parseInt(this.state.message.id) === item.id &&

    {this.state.message.message}

    } -``` - -Finally we need to set the new state in the `addToOrder` function - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) - this.setState({message: {id: id, message: result.data.message}}) -} -``` - -If we run our tests now everything should go green. - -Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. - -```bash -$ touch spec/requests/api/client_can_create_new_order_spec.rb -``` - -Add the following test to the spec file - -```ruby -RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: 'Pizza') } - let!(:product_2) { create(:product, name: 'Kebab') } - - it 'responds with success message' do - post '/api/orders', params: {id: product_1.id } - - expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' - end -end -``` -Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. - -Run the test and make sure that you are getting the correct error messages. - -We will probably get an error message that states that we have an unitialized constant Api::OrdersController. - -We will fix this by generating a new orders controller. - -`$ rails generate controller Api::Orders create` - -Run the test again and you will get a new error message. - - -The error message probably has to do with our routes and that there are none. Lets fix that. - -```ruby -Rails.application.routes.draw do - namespace :api do - resources :products, only: [:index] - resources :orders, only: [:create, :update] - end -end -``` - -Our problem now is that we are not getting the right response. But this is expected. - -To fix this we need to add the order to the create action in our Orders controller. - -```ruby -class Api::OrdersController < ApplicationController - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` - -The next step we need to take is to generate a Order model to have something to pull out from the database. - -`$ rails generate model order` - -We also need to associate the the order to the order items that we have, and the products. - -`$ rails generate model OrderItems order:references product:references` - -Go ahead and go through the files that are generated and make sure that there are no problems. - -We will now add specs to test the associations we created between the models. -In the newly generated `order_items_spec.rb` go ahead and add two specs. -You will need more than that but for this guide we will limit ourselves to these specs. - -```ruby -require 'rails_helper' - -RSpec.describe OrderItem, type: :model do - it { is_expected.to belong_to :order } - it { is_expected.to belong_to :product } -end -``` - -`order_spec.rb` - -```ruby -require 'rails_helper' - -RSpec.describe Order, type: :model do - it {is_expected.to have_many :order_items} -end -``` - -We need to make sure that the associations are present as well. - -`order_item.rb` - -```ruby -class OrderItem < ApplicationRecord - belongs_to :order - belongs_to :product -end -``` - -`order.rb` - -```ruby -class Order < ApplicationRecord - has_many :order_items -end -``` - -Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. - - ------- - -### Returning to the client - -Let's go back tot the client and add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. - -We need to do a couple of things: - -Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. - -We will also add a `PUT` request to this block in order to update the order that has already been created. - -And at last we will add a test to check that we can add multiple products to an order. - -The final form of the feature will be the following: - -```js -describe('User can add a product to his/her order', () => { - - beforeEach(() => { - cy.server(); - cy.route({ - method: 'GET', - url: 'http://localhost:3000/api/products', - response: 'fixture:product_data.json' - }) - - cy.route({ - method: 'POST', - url: 'http://localhost:3000/api/orders', - response: { message: 'The product has been added to your order', order_id: 1 } - }) - - cy.route({ - method: 'PUT', - url: 'http://localhost:3000/api/orders/1', - response: { message: 'The product has been added to your order', order_id: 1 } - }) - cy.visit('http://localhost:3001') -}); - - it('user get a confirmation message when adding product to order', () => { - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - }); -}); -``` - -Make sure that you have added the product_id to the messages in the `beforeEach` function. - -Lets continue with the implementation code to get all of this to work. -add the `orderId` to the state -```js -state = { - productData: [], - message: {}, - orderId: '' - } -``` - -Now we need to update the `addToOrder` function with the new `orderId` state. - - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) - this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) -} -``` -The `order_id` is what is being returned to us from the backend. - -We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. - -```js - async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result - if (this.state.orderId !== "") { - result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) - } else { - result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - } - this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) - } -``` - -So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. - -Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. - - -Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. - -We want to make sure that we can view the products that we have added to our order. - -First we want to make sure that there is a button where we can view the order. -However this button should only be visable when we have added products to our order. - -Refactor your code to look like this: - -```js - [....] - - it('user can add multiple products to order and view its content', () => { - cy.get('button').contains('View order').should('not.exist') // Add this line - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('button').contains('View order').should('exist') // Add this line - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - }); -}); -``` - -And now for the implementation, let's add the button in the `DisplayProducts` component - -```js -return ( - <> - {this.state.orderId !== '' && - } - {this.state.showOrder && -
      - {orderDetailsDisplay} -
    - } - {dataIndex} - -``` - - ### Update the order Backend - - diff --git a/guides/react_checkout_guide/07_some_refactoring.md b/guides/react_checkout_guide/07_some_refactoring.md index b9dfb62f..7682aa7e 100644 --- a/guides/react_checkout_guide/07_some_refactoring.md +++ b/guides/react_checkout_guide/07_some_refactoring.md @@ -1,14 +1,218 @@ - Some quick refactoring -Lets go back to our test and refactor them a little +Let's go back to our test and refactor them a little. +We want to use fixture files in order to mock the responses that we should be getting, instead of having the information our feature files. -Add the fixture files to the test +Start by creating to fixtures, one for the put request and one for the post request. ```bash $ touch cypress/fixtures/put_response.json $ touch cypress/fixtures/post_response.json ``` -zap i should have committed a loong time ago -(update this with the code from the client) \ No newline at end of file +Add the fixture files to the test + +`cypress/fixtures/post_response.json` + +```json +{ + "message": "The product has been added to your order", + "order_details": { + "order": { + "id": 1, + "products": [ + { + "name": "Salad", + "price": 4.0 + } + ] + } + } +} +``` + +And the other in the `cypress/fixtures/put_response.json` + +```json +{ + "message": "The product has been added to your order", + "order_details": { + "order": { + "id": 1, + "products": [ + { + "name": "Salad", + "price": 4.0 + }, + { + "name": "Ice Cream", + "price": 3.75 + } + ] + } + } +} +``` + +You also need to refactor your tests to look like this: + +```js +describe("User can add a product to his/her order", () => { + beforeEach(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json", + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: "fixture:post_response.json", + }); + + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: "fixture:put_response.json", + }); + cy.visit("http://localhost:3001"); + }); + + it("user can add multiple product to order and view its content", () => { + cy.get("button").contains("View order").should("not.exist"); + + cy.get("#product-2").within(() => { + cy.get("button").contains("Add to order").click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + + cy.get("button").contains("View order").should("exist"); + + cy.get("#product-3").within(() => { + cy.get("button").contains("Add to order").click(); + cy.get(".message").should( + "contain", + "The product has been added to your order" + ); + }); + + cy.get("button").contains("View order").click(); + + cy.get("#order-details").within(() => { + cy.get("li").should("have.length", 2); + }); + + cy.get("button").contains("View order").click(); + + cy.get("#order-details").should("not.exist"); + }); +}); +``` + +And finally our implementation code: +It should look like this ater you have refactored it. + +```js +import React, { Component } from "react"; +import { getData } from "../modules/productData"; +import axios from "axios"; + +class DisplayProductData extends Component { + state = { + productData: [], + message: {}, + orderDetails: {}, + showOrder: false, + }; + + componentDidMount() { + this.getProductData(); + } + + async getProductData() { + let result = await getData(); + this.setState({ productData: result.data.products }); + } + + async addToOrder(event) { + let id = event.target.parentElement.dataset.id; + let result; + if (this.state.orderDetails.hasOwnProperty("id")) { + result = await axios.put( + `http://localhost:3000/api/orders/${this.state.orderDetails.id}`, + { product_id: id } + ); + } else { + result = await axios.post("http://localhost:3000/api/orders", { + product_id: id, + }); + } + this.setState({ + message: { id: id, message: result.data.message }, + orderDetails: result.data.order_details.order, + }); + } + + render() { + let dataIndex, orderDetailsDisplay; + if ( + Array.isArray(this.state.productData) && + this.state.productData.length + ) { + dataIndex = ( +
    + {this.state.productData.map((item) => { + return ( +
    + {`${item.name} ${item.description} ${item.price}`} + + {parseInt(this.state.message.id) === item.id && ( +

    {this.state.message.message}

    + )} +
    + ); + })} +
    + ); + } + if (this.state.orderDetails.hasOwnProperty("products")) { + orderDetailsDisplay = this.state.orderDetails.products.map((item) => { + return
  • {item.name}
  • ; + }); + } else { + orderDetailsDisplay = "Nothing to see"; + } + + return ( + <> + {this.state.orderDetails.hasOwnProperty("products") && ( + + )} + {this.state.showOrder && ( +
      {orderDetailsDisplay}
    + )} + {dataIndex} + + ); + } +} +export default DisplayProductData; +``` + +Run the test and move to the next section. diff --git a/react-cart-updated.md b/react-cart-updated.md deleted file mode 100644 index 73426d1e..00000000 --- a/react-cart-updated.md +++ /dev/null @@ -1,646 +0,0 @@ -## Adding cart functionality to React - -In this section we are going to add checkout functionality to our react application. -The prerequsitet for this functionality is that we have products on our page that are ready to sell. - -Start with making sure that you have the latest code pulled from github on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. - -As always we will work in a test driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. - - -Create a new feature file by typing -`$ touch cypress/integration/userCanAddProductsToOrder.feature.js` - -Add the following tests to the file: - -```js -describe("User can add a product to their order", () => { - before(() => { - cy.server(); - cy.route({ - method: "GET", - url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" - }); - - cy.route({ - method: "POST", - url: "http://localhost:3000/api/orders", - response: { message: "A product has been added to your order" } - }); - }); - - it("user gests a confirmation messsage when adding a a producut to order", () => { - cy.visit("http://localhost:3001"); // Make sure to point it to port 3001 as we are using 3000 for our API - cy.get("#product-1").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - }); - cy.wait(3000) - }); -}); -``` -So what are we excpecting to see in the implementation. Well we need to make sure that we can add a product to an order. This means that we need to have a list of products since before. After that we need to make sure that there is a button to make this happen. And it will all end with us getting a confirmation message that we have added a product to our order. Simple huh? - -When you run your test now it should all fail and in order to make it work we have to add the actual implementaion code. - -In our case we already have a component that we use in order to display the products, all we have to do now is to add the button to the iteration and then make sure that we can see the message. - -Let's add the implementation code. - -We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are maping over the products. - -```jsx -
    - {`${item.name} ${item.description} ${item.price}`} - -
    -``` -So here we have added a `addToOrder` function that is not defined. Lets go ahead and add that functionality as well. - -Add the following code to the component: - -```js -addToOrder() { - debugger -} -``` - -So why are we adding a debugger here? Well first we need to make sure that the `addToOrder` button actually gets invoked. - -Run your tests now. The debugger should kick in and halt the execution, if not then make sure that you have added React developer tools to your chrome broweser. - -If we can see that then its time for us to add the real functionality. -But first lets make sure that we are hitting the right button. -Go to your chrome console where the execution of the code has stopped and run the following command in the console: - -`event.target.parentElement` - -You should probably see something like this in your console: - -```html -
    - "Pizza" - "Crusty and fluffy" - "100 SEK" - -
    -``` -If not then make sure that you are iterating over your products properly. - -So this was our first iteration. But we need to make sure that we are pulling the information of the product as a dataset rather than through an id, and the reason for that is that it will be easier for us to manage. But it will also help us use it beacuse the values in the product are sent back to us in the form of an object. - -Add the dataset to the parent div: - -```js -
    - {`${item.name} ${item.description} ${item.price}`} - -
    -``` - -Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. -you should see the same information as before, but the information about the product will be returend to us in the form of an object. - -This is good new for us beacuse we can use this in our `addToOrder` function. Time to add the rest of the implementation code to the `addToOrder` function. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) -} -``` - -So what did we do here? First we made the method asyncronous beacuse we need to wait for a response from our API, we passed in the event as an argument to the function and we also added axios in order to handle that call to the API. the last thing that we added was a post request that we save in the variable `result` - -Make sure that you add axios and also import it to the top of the file if you haven't already. - -The next thing we are going to focus on is to get the message to be displayed. - -Lets start with adding the message to the state: - -```js -state = { - productData: [], - message: {} - } -``` - -Next we need to display the message below the button. - -```js -[....] - - {parseInt(this.state.message.id) === item.id &&

    {this.state.message.message}

    } -``` - -Finally we need to set the new state in the `addToOrder` function - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) - this.setState({message: {id: id, message: result.data.message}}) -} -``` - -If we run our tests now everything should go green. - -Lets move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. - -```bash -$ touch spec/requests/api/client_can_create_new_order_spec.rb -``` - -Add the following test to the spec file - -```ruby -RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: 'Pizza') } - let!(:product_2) { create(:product, name: 'Kebab') } - - it 'responds with success message' do - post '/api/orders', params: {id: product_1.id } - - expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' - end -end -``` -Remember that the request spec acts like our feature test but for APIs. This will allow us to see what what requests we are sending out. - -Run the test and make sure that you are getting the correct error messages. - -We will probably get an error message that states that we have an unitialized constant Api::OrdersController. - -We will fix this by generating a new orders controller. - -`$ rails generate controller Api::Orders create` - -Run the test again and you will get a new error message. - - -The error message probably has to do with our routes and that there are none. Lets fix that. - -```ruby -Rails.application.routes.draw do - namespace :api do - resources :products, only: [:index] - resources :orders, only: [:create] - end -end -``` - -Our problem now is that we are not getting the right response. But this is expected. - -To fix this we need to add the order to the create action in our Orders controller. - -```ruby -class Api::OrdersController < ApplicationController - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` - -The next step we need to take is to generate a Order model to have something to pull out from the database. - -`$ rails generate model order` - -We also need to associate the the order to the order items that we have, and the products. - -`$ rails generate model OrderItems order:references product:references` - -Go ahead and go through the files that are generated and make sure that there are no problems. - -We will now add specs to test the associations we created between the models. -In the newly generated `order_items_spec.rb` go ahead and add two specs. -You will need more than that but for this guide we will limit ourselves to these specs. - -```ruby -require 'rails_helper' - -RSpec.describe OrderItem, type: :model do - it { is_expected.to belong_to :order } - it { is_expected.to belong_to :product } -end -``` - -`order_spec.rb` - -```ruby -require 'rails_helper' - -RSpec.describe Order, type: :model do - it {is_expected.to have_many :order_items} -end -``` - -We need to make sure that the associations are present as well. - -`order_item.rb` - -```ruby -class OrderItem < ApplicationRecord - belongs_to :order - belongs_to :product -end -``` - -`order.rb` - -```ruby -class Order < ApplicationRecord - has_many :order_items -end -``` - -Run your migrations and then run your specs. Everything should be green like Bruce Banners alter ego. - - ------- - -### Update the order Backend [cart react p3] - -First we need to amend our previous spec. - -We will amend our specs by adding a before block -```ruby - before do - post '/api/orders', params: { id: product_1.id } - @order = Order.last - end -``` - -And adding a new spec - -```ruby - it 'adds another product to order if param "order_id" is present' do - post '/api/orders', params {id: product_2.id, order_id: @order.id} - expect(@order.order_items.count).to eq 2 - end -``` -In order to get this to work in the first iteration we can simply refactor our create action in the controller. - -```ruby -class Api::OrdersController < ApplicationController - def create - order = if params[:order_id] - Order.find(params[:order_id]) - else - Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` - -Run your test and make sure that this is going green. This is working however this is not the best practice. - -Let's make sure that we follow the conventions and refactor this code so it works. - -Lets refactor our test first -```ruby - it 'adds another product to order if param "order_id" is present' do - put "/api/orders/#{order.id}", params {product_id: product_2.id } - expect(@order.order_items.count).to eq 2 - end -``` - -You will probably get an routing error. We will fix that with adding update to the routes: - - `resources :orders, only: [:create, :update]` - -Run the test again and you will get a error stating that there is no action update. - -```ruby - def update - order = Order.find(params[:id]) - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: { message: 'The product has been added to your order'} - end -``` - -And remember to clean out the create action to its previous state: - -```ruby - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order' } - end -``` - -We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. - -```rb -RSpec.describe Api::OrdersController, type: :request do - let!(:product_1) { create(:product, name: 'Pizza') } - let!(:product_2) { create(:product, name: 'Kebab') } - - before do - post '/api/orders', params: { product_id: product_1.id } - @order_id = JSON.parse(response.body)['order_id'] - end - - describe 'POST /api/orders' do - it 'responds with success message' do - expect(JSON.parse(response.body)['message']).to eq 'The product has been added to your order' - end - - it 'responds with order id' do - order = Order.find(@order_id) - expect(JSON.parse(response.body)['order_id']).to eq order.id - end - end - - describe 'PUT /api/orders/:id' do - before do - put "/api/orders/#{@order_id}", params: { product_id: product_2.id } - @order = Order.find(@order_id) - end - - it 'adds another product to order if request is a PUT and param id of the order is present' do - expect(@order.order_items.count).to eq 2 - end - - it 'responds with order id' do - expect(JSON.parse(response.body)['order_id']).to eq @order.id - end - end -end -``` - -And in the controller: - -```rb -class Api::OrdersController < ApplicationController - def create - order = Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end - - def update - order = Order.find(params[:id]) - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: { message: 'The product has been added to your order', order_id: order.id } - end -end -``` - - -### Putting it togheter - -In order for the client to work with the backend we need to make some adjustment to the code. -In your clinent make sure that you add product_id instead of the id. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - this.setState({message: {id: id, message: result.data.message}}) -} -``` - -### Returning to the client - -Let's go back tot the client and add the functionality to add multiple products to our order. As always we need to have feature test for that. We will add this to the test that we already have. - -We need to do a couple of things: - -Instead of having a `before` function that only runs for one test, we will add a `beforeEach` that will run before all the upcoming feature test. - -We will also add a `PUT` request to this block in order to update the order that has already been created. - -And at last we will add a test to check that we can add multiple products to an order. - -The final form of the feature will be the following: - -```js -describe('User can add a product to his/her order', () => { - - beforeEach(() => { - cy.server(); - cy.route({ - method: 'GET', - url: 'http://localhost:3000/api/products', - response: 'fixture:product_data.json' - }) - - cy.route({ - method: 'POST', - url: 'http://localhost:3000/api/orders', - response: { message: 'The product has been added to your order', order_id: 1 } - }) - - cy.route({ - method: 'PUT', - url: 'http://localhost:3000/api/orders/1', - response: { message: 'The product has been added to your order', order_id: 1 } - }) - cy.visit('http://localhost:3001') -}); - - it('user get a confirmation message when adding product to order', () => { - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - }); -}); -``` - -Make sure that you have added the product_id to the messages in the `beforeEach` function. - -Lets continue with the implementation code to get all of this to work. -add the `orderId` to the state -```js -state = { - productData: [], - message: {}, - orderId: '' - } -``` - -Now we need to update the `addToOrder` function with the new `orderId` state. - - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { id: id } ) - this.setState({message: {id: id, message: result.data.message}, orderId: results.data.order_id}) -} -``` -The `order_id` is what is being returned to us from the backend. - -We also need to make sure that we are adding a order to an existing one, if one already exists so lets add a conditional for that. - -```js - async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result - if (this.state.orderId !== "") { - result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) - } else { - result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - } - this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) - } -``` - -So what does this conditional do? We are simply stating that if there is a order and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise go ahead and create a new order. - -Run your feature test they should no go green. Go ahead and test this manually aswell. Remember to fire up both the backend and frontend servers. You also need to have some products added to your backend. - - -Now that we have the main functionality in place we need to start tweaking and making sure that the user experiance is good. - -We want to make sure that we can view the products that we have added to our order. - -First we want to make sure that there is a button where we can view the order. -However this button should only be visable when we have added products to our order. - -Refactor your code to look like this: - -```js - [....] - - it('user can add multiple products to order and view its content', () => { - cy.get('button').contains('View order').should('not.exist') // Add this line - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('button').contains('View order').should('exist') // Add this line - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - }); -}); -``` - -And now for the implementation, let's add the button in the `DisplayProducts` component - -```js -return ( - <> - {this.state.orderId !== '' && - } - {this.state.showOrder && -
      - {orderDetailsDisplay} -
    - } - {dataIndex} - -``` - -### React p5 - -Lets work on the view order - -Go to the client, first we willl write the test - -```js - it('user can add multiple product to order and view its content', () => { - cy.get('button').contains('View order').should('not.exist') - - cy.get('#product-2').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('button').contains('View order').should('exist') - - cy.get('#product-3').within(() => { - cy.get('button').contains('Add to order').click() - cy.get('.message').should('contain', "The product has been added to your order") - }) - - cy.get('button').contains('View order').click() - cy.get('#order-details').within(()=> { - cy.get('li').should('have.length', 2) - }) - cy.get('button').contains('View order').click() - cy.get('#order-details').should('not.exist') - - }); - ``` - - Run the test and it is failing. - - ```js -return ( - <> - {this.state.orderDetails.hasOwnProperty('products') && - - } - {this.state.showOrder && -
      - {orderDetailsDisplay} -
    - } - {dataIndex} - -``` -check if the tests are going green - -commit now -zap i should have committed a loong time ago - -Lets move go back to our backend to continue this functionality - - - From c5ccff30faaf80e08b6702de8d8f760fbdff07fd Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Tue, 7 Apr 2020 15:56:01 +0200 Subject: [PATCH 16/19] Adds stripe guide --- .../11_finializing_on_the_client_side.md | 4 +- .../12_intro_to_stripe.md | 70 +++ .../13_setting_the_stage.md | 57 ++ .../14_back_to_the_tests.md | 90 +++ .../15_the_payment_form.md | 42 ++ .../16_adding_multiple_input_field.md | 135 ++++ .../17_stripe_in_the_backend.md | 96 +++ .../18_adding_credentials.md | 76 +++ .../19_finalizing_the_payment.md | 37 ++ stripe_react_rails.md | 586 ------------------ 10 files changed, 606 insertions(+), 587 deletions(-) create mode 100644 guides/react_checkout_guide/12_intro_to_stripe.md create mode 100644 guides/react_checkout_guide/13_setting_the_stage.md create mode 100644 guides/react_checkout_guide/14_back_to_the_tests.md create mode 100644 guides/react_checkout_guide/15_the_payment_form.md create mode 100644 guides/react_checkout_guide/16_adding_multiple_input_field.md create mode 100644 guides/react_checkout_guide/17_stripe_in_the_backend.md create mode 100644 guides/react_checkout_guide/18_adding_credentials.md create mode 100644 guides/react_checkout_guide/19_finalizing_the_payment.md delete mode 100644 stripe_react_rails.md diff --git a/guides/react_checkout_guide/11_finializing_on_the_client_side.md b/guides/react_checkout_guide/11_finializing_on_the_client_side.md index 73aa7c85..eced6c0e 100644 --- a/guides/react_checkout_guide/11_finializing_on_the_client_side.md +++ b/guides/react_checkout_guide/11_finializing_on_the_client_side.md @@ -130,4 +130,6 @@ And finally the render function: } ``` -Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. Go over your code again and marvel at your creation. This is one approach we can take to create an ordering functionality in React from scratch. \ No newline at end of file +Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. Go over your code again and marvel at your creation. This is one approach we can take to create an ordering functionality in React from scratch. + +The last thing we need to do is to add our payment gateway. diff --git a/guides/react_checkout_guide/12_intro_to_stripe.md b/guides/react_checkout_guide/12_intro_to_stripe.md new file mode 100644 index 00000000..b7e21fa2 --- /dev/null +++ b/guides/react_checkout_guide/12_intro_to_stripe.md @@ -0,0 +1,70 @@ + +## Intro to Stripe + +This guide needs to have the order functionality from the previous chapter in place. We will continue on the code both from the backend as well as from the frontend. + +We are going to use stripe to finalize the order by paying for the products. + +Stripe is a payment gateway that is developer-friendly. +In this section, we are going to set up the functionality that we need in order to make the payments with debit or credit card. + +First and foremost you need to create an account on stripe. We will get back to this later. But make sure that you have an account ready. So let's set the stage, create a new branch and let's get started. + +As with everything that we develop here at Craft Academy we work in a test-driven way. This means that we always start with our feature test to stay in scope but also make sure that we are not breaking any functionality that has already been added to the previous features. + +Start by creating a new feature by running the following command in your terminal. + +`$ touch cypress/integration/userCanMakePayment.feature.js` + +After the file has been created we need to add the following test. + +```js +describe("User can add a product to his/her order", () => { + beforeEach(() => { + cy.server(); + cy.route({ + method: "GET", + url: "http://localhost:3000/api/products", + response: "fixture:product_data.json" + }); + + cy.route({ + method: "POST", + url: "http://localhost:3000/api/orders", + response: "fixture:post_response.json" + }); + + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + response: "fixture:put_response.json" + }); + + cy.visit("http://localhost:3001"); + cy.get("#product-2").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("#product-3").within(() => { + cy.get("button") + .contains("Add to order") + .click(); + }); + cy.get("button") + .contains("View order") + .click(); + }); + + it("user can pay for his order", () => { + cy.get("button") + .contains("Confirm!") + .click(); + cy.get("#payment-form").should("exist"); + }); +}); +``` +So what is happening in this test? +Well, we are setting the stage and making sure that we can display a payment form. Remember that we always work in small chunks and that we continually refactor both our test but also our implementation code as we add more and more functionality. + +Let's head over to the component that we want to display our payment form. diff --git a/guides/react_checkout_guide/13_setting_the_stage.md b/guides/react_checkout_guide/13_setting_the_stage.md new file mode 100644 index 00000000..b5004780 --- /dev/null +++ b/guides/react_checkout_guide/13_setting_the_stage.md @@ -0,0 +1,57 @@ +## Setting the stage + +Start by adding a new state, where we are adding the payment form. Why are we setting it to false? + +```js +state = { + productData: [], + message: {}, + orderDetails: {}, + showOrder: false, + orderTotal: "", + showPaymentform: false +}; +``` + +After that, we need to refactor the button to show the form when the user clicks on it. + + +```js +; +{ + this.state.showPaymentform && ( +
    + +
    + ); +} +``` +Remember to also import the on the top of the file. +But we don't have a PaymentForm component you are thinking. Well, that's right. We don't so... +Let's create one. + +```bash +$ touch src/components/PaymentForm.jsx +``` + +```js +import React, { Component } from "react"; + +class PaymentForm extends Component { + render() { + return ( +
    +

    Here we will show a payment form

    +
    + ); + } +} + +export default PaymentForm; +``` +Run your test, commit and all that fun stuff. +Just to check, you did remember to create a new branch on git before we started right? Of course you did. silly me.... + + diff --git a/guides/react_checkout_guide/14_back_to_the_tests.md b/guides/react_checkout_guide/14_back_to_the_tests.md new file mode 100644 index 00000000..9915d23b --- /dev/null +++ b/guides/react_checkout_guide/14_back_to_the_tests.md @@ -0,0 +1,90 @@ +## Back to our tests + +Let's revisit our test for one moment. When we are dealing with stripe they provide us with a lot of functionality in terms of security and checks. This is good for development but could be an issue when we are trying to test the payment functionality. We need to test that we can see an iframe where we will be displaying the payment form. + +Refactor your test to look like this. + +```js +it("user can pay for his order", () => { + cy.get("button") + .contains("Confirm!") + .click(); + cy.get("#payment-form").should("exist"); + cy.wait(1000); + cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="cardnumber"]') + .type("4242424242424242", { delay: 50 }); + }); +}); +``` + +We are finding an input field and filling it out with the card number. Small steps remember. Run your test and behold the error messages. + +Time for the implementation + +Add the following package. + +```bash +$ yarn add react-stripe-elements +``` + +To use stripe we also need to add a script provided by stripe. + +Add a script to the head in index.html + +```html + +``` + +And we also need to turn off some of the built-in settings on cypress just to make sure that we are not getting errors because of our browser. + +Setting chromeWebSecurity to false in Chrome-based browsers allows you to do the following: + +- Display insecure content +- Navigate to any superdomain without cross-origin errors +- Access cross-origin iframes that are embedded in your application + +Add `chromeWebSecurity` and set it to `false` + +`cypress.json` + +```json +{ + "baseUrl": "http://localhost:3001", + "chromeWebSecurity": false +} +``` +Head over to your `index.js` and import the StripeProvider. + +```js +import { StripeProvider } from "react-stripe-elements"; +``` + +And wrap component with the StripeProvider + +```js +ReactDOM.render( + + + , + document.getElementById("root") +); +``` + +The StripeProvider gives us access to the Stripe object. The Stripe object will contain the secret, and publishable key which will allow us to access the Stripe API. + +Get your API key from the stripe dashboard, make sure that you select "Show test data" and go to the developer's tab. Copy your Publishable key and add it to the stripe provider in your App component. + +```js +ReactDOM.render( + + + , + document.getElementById("root") +); +``` + +Time to start working on the payment form itself. + diff --git a/guides/react_checkout_guide/15_the_payment_form.md b/guides/react_checkout_guide/15_the_payment_form.md new file mode 100644 index 00000000..6bde7f18 --- /dev/null +++ b/guides/react_checkout_guide/15_the_payment_form.md @@ -0,0 +1,42 @@ +## The payment form + +Wrap the PaymentForm component with the Elements, and make sure that you import Elements from `"react-stripe-elements"` + +```js +{ + this.state.showPaymentform && ( +
    + + + +
    + ); +} +``` + +Move over to your PaymentForm and update it with the following code. + +```js +import React, { Component } from "react"; +import { injectStripe, CardNumberElement } from "react-stripe-elements"; + +class PaymentForm extends Component { + render() { + return ( + <> + + + + ); + } +} + +export default injectStripe(PaymentForm); +``` + +So what are we doing here? We are adding an input field where the users can add their card number but to use that we need to use the injectStripe component to make use of their form elements. + +Run the test now and they should be green. If they do, commit. + +So we have added a rudimentary payment form and we have also added the package that we need to use stripe in our react app. But we need multiple input fields for the rest of our card information. So let's add that now. + diff --git a/guides/react_checkout_guide/16_adding_multiple_input_field.md b/guides/react_checkout_guide/16_adding_multiple_input_field.md new file mode 100644 index 00000000..2a5d845c --- /dev/null +++ b/guides/react_checkout_guide/16_adding_multiple_input_field.md @@ -0,0 +1,135 @@ +## Adding multiple input fields in the payment form + +We need to update our test again for the remaining input fields, as well as with the assertion that the payment has gone through. + +```js +it("user can pay for his order", () => { + cy.route({ + method: "PUT", + url: "http://localhost:3000/api/orders/1", + body: { activity: "finalize" }, + response: { paid: true, message: "Your order will be ready in 30 minutes!" } + }); + cy.get("button") + .contains("Confirm!") + .click(); + + cy.get("#payment-form").should("exist"); + cy.wait(1000); + cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="cardnumber"]') + .type("4242424242424242", { delay: 50 }); + }); + + cy.get('iframe[name^="__privateStripeFrame6"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="exp-date"]') + .type("1222", { delay: 10 }); + }); + cy.get('iframe[name^="__privateStripeFrame7"]').then($iframe => { + const $body = $iframe.contents().find("body"); + cy.wrap($body) + .find('input[name="cvc"]') + .type("999", { delay: 10 }); + }); + + cy.get("button") + .contains("Submit") + .click(); + + cy.get("#payment-form").should("not.exist"); + cy.get(".message").should( + "contain", + "Your order will be ready in 30 minutes!" + ); +}); +``` + +Let us go back to your PaymentForm component and update the return with the rest of the payment fields. Make sure that you import the + +```js + return ( + <> + + + + + + + + +``` + +Next we want to add a function called `payWithStripe` + +```js +async payWithStripe() { + await this.props.stripe.createToken().then(response => { + if (response.token) { + try { + this.performPayment(response.token) + } + catch { + + } + } + + }) + } +``` + +We also want to add a function called performPayment. This function will make the payment. + +```js + async performPayment(token) { + let orderResponse = await axios.put( + `http://localhost:3000/api/orders/${this.props.orderDetails.id}`, + { + activity: 'finalize', + stripeToken: token + } + ) + if (orderResponse.data.paid === true) { + this.props.finalizeOrder(orderResponse.data.message) + + } else { + debugger + } + } +``` + +And finally, we need to update the button + +```js + +``` + +Make sure that you have made adequate imports to the component. + +Move over to the DisplayProduct component or your main component where you are adding the products to the order. + +Update the Props that you are sending through + +```js + +``` + +And update the finalizeOrder function we are passing in the finializeOrder function as a prop to the component. + +```js + async finalizeOrder(message) { + let orderTotal = this.state.orderDetails.order_total + this.setState({ message: { id: 0, message: message }, orderTotal: orderTotal, orderDetails: {}, showPaymentForm: false }) + } +``` + +Run your tests and they should go green. + +Let's move over to the backend + diff --git a/guides/react_checkout_guide/17_stripe_in_the_backend.md b/guides/react_checkout_guide/17_stripe_in_the_backend.md new file mode 100644 index 00000000..c895fa84 --- /dev/null +++ b/guides/react_checkout_guide/17_stripe_in_the_backend.md @@ -0,0 +1,96 @@ +## Stripe in the Backend + +Time to work on the backend. Make sure that you create a new branch here as well before you get started. + +Start with creating a request spec. + +```bash +$ touch spec/requests/api/client_can_send_payment_request_spec.rb +``` + +Add the following test + +```rb +RSpec.describe Api::OrdersController, type: :request do + let(:product_1) { create(:product, price: 50) } + let(:order) { create(:order) } + let!(:order_item) { create(:order_item, product: product_1, order: order) } + + before do + put "/api/orders/#{order.id}", + params: { activity: "finalize", + stripetoken: "12345", + email: 'testing@test.com' + } + end + + it '' do + expect(JSON.parse(response.body)).to eq JSON.parse('{"paid": true, "message": "Your order will be ready in 30 minutes!"}') + end +end +``` + +Run your tests and the should be failing. That is a good thing because now we know what to do next. + +Head over to your orders controller and add the following functionality to the update action + +```rb + def update + order = Order.find(params[:id]) + if params[:activity] + payment_status = perform_stripe_payment + if payment_status == true + # successful payment + order.update_attribute(:finalized, true) + render json: { paid: true, message: 'Your order will be ready in 30 minutes!' } + + else + # unsuccessful payment + end + + else + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) + end + end +``` +We are not dealing with the unsuccessful payment. Make sure that you add and test for an error message once we are done with the Happy path. + +You will get a Name error stating that there is no `perform_stripe_payment` + +Let's fix that. In the private section of the `orders` controller add the following method + +```rb + def perform_stripe_payment + order = Order.find(params[:id]) + customer = Stripe::Customer.create( + email: params[:email], + source: params[:stripeToken], + description: 'slowfood client' + ) + + charge = Stripe::Charge.create( + customer: customer.id, + amount: ordr.order_total, + currency: 'sek' + ) + charge + end +``` +We are calling the Stripe gem to create a customer but also to make charge the customer. + +You will be getting errors now, we need to fix that by adding the stripe gem to our Gemfile. + +`gem 'stripe-rails'` + +Run the tests again and your new error message should something in the lines of + +```bash + Stripe::AuthenticationError: + No API key provided. Set your API key using "Stripe.api_key = ". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions. +``` + +This means that we need to use our API key that Stripe has provided for us. + + diff --git a/guides/react_checkout_guide/18_adding_credentials.md b/guides/react_checkout_guide/18_adding_credentials.md new file mode 100644 index 00000000..0954b7e8 --- /dev/null +++ b/guides/react_checkout_guide/18_adding_credentials.md @@ -0,0 +1,76 @@ +## Adding credentials + +Grab the "secret key" from your Stripe dashboard. + +Do not reveal this key. To use this securely we will add the key to our credentials file. Credentials is an encrypted file in rails. We need the master.key file in our config folder to open this file. The master.key is added to your gitignore by default so that you don\t accidentally push it to version control. + +Use the following steps to modify your credentials. + +Head over to your terminal and run the following command. + +```bash +$ EDITOR='code --wait' rails credentials:edit +``` +Notice that your terminal has been locked and the credentials file has been decrypted and opened in your code editor. The terminal will be locked until you save and close the credentials file in your code editor. But before we do that we need to add the secret key and publishable key in this file. + + +```yml +secret_key_base: 50fba2642c2978bcf7536a53606328149f18c7382a070822bf86f0d141095d9ed2c2c472b77577df08c34c61e36f3a27aaba26fb746cfb6a79ce3456edbcb04b +stripe: + pk_key: your_publishable_key_here + secret_key: your_secret_key_here +``` + +Save and close the file and you should be getting the following message in the terminal. + +```bash +16:16 $ EDITOR='code --wait' rails credentials:edit +File encrypted and saved. +``` + +To make use of the stripe credentials we need to add some configurations. + +Head over to the `application.rb` file where we turn off the generators etc and add the following configurations. + +```rb + config.generators do |generate| + generate.helper false + generate.assets false + generate.view_specs false + generate.helper_specs false + generate.routing_specs false + generate.controller_specs false + end + config.stripe.secret_key = Rails.application.credentials.stripe[:secret_key] + config.stripe.publishable_key = Rails.application.credentials.stripe[:pk_key] + end +``` + +We do not want to make actual network calls to stripe when we are in development mode. For that we will use a gem called `stripe-ruby-mock`. Make sure that you use the version stated below. + +Add the following gem to the Gemfile + +`gem 'stripe-ruby-mock', '~> 2.5.6', require: 'stripe_mock'` + +Then we want to update the rails_helper to add the mock functionality + +Add `require 'stripe_mock'` to the rails_helper and add the configuration to RSpec + +```rb +RSpec.configure do |config| + config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! + config.include FactoryBot::Syntax::Methods + config.include(Shoulda::Matchers::ActiveRecord, type: :model) + config.before(:each) do + @stripe_test_helper = StripeMock.create_test_helper + StripeMock.start + end + config.after(:each) do + StripeMock.stop + end +end +``` + diff --git a/guides/react_checkout_guide/19_finalizing_the_payment.md b/guides/react_checkout_guide/19_finalizing_the_payment.md new file mode 100644 index 00000000..f06bc082 --- /dev/null +++ b/guides/react_checkout_guide/19_finalizing_the_payment.md @@ -0,0 +1,37 @@ +## Finalizing the payment + +Next up we want to modify the test with the mocked data. + +```rb + before do + put "/api/orders/#{order.id}", + params: { activity: "finalize", + stripeToken: StripeMock.create_test_helper.generate_card_token, + email:'user@mail.com'} + end +``` + +And finally update the perform_stripe_payment method in our controller. + +```rb + def perform_stripe_payment + order = Order.find(params[:id]) + customer = Stripe::Customer.create( + email: params[:email], + source: params[:stripeToken], + description: 'slowfood client' + ) + + charge = Stripe::Charge.create( + customer: customer.id, + amount: order.order_total.to_i * 100, + currency: 'sek' + ) + charge.paid + end + ``` + + Run your tests now and they should be going green. + So Stripe added. + Let's celebrate by making a commit! + diff --git a/stripe_react_rails.md b/stripe_react_rails.md deleted file mode 100644 index 62149186..00000000 --- a/stripe_react_rails.md +++ /dev/null @@ -1,586 +0,0 @@ -This guide needs to have the order functionality from the previous chapter in place. We will continue on the code both from the backend as well as from the frontend. - -We are going to use stripe to finalize the order by paying for the products. - -Stripe is a payment gateway that is developer-friendly. In this section, we are going to set up the functionality that we need in order to make the payments with debit or credit card. - -First and foremost you need to create an account on stripe. We will get back to this later. But make sure that you have an account ready. So let's set the stage, create a new branch and let's get started. - -As wit everything that we develop here at Craft Academy we work in a test-driven way. This means that we always start with our feature test to stay in scope but also make sure that we are not breaking any functionality that has already been added to the previous features. - -Start by creating a new feature by running the following command in your terminal. -`$ touch cypress/integration/userCanMakePayment.feature.js` - -After the file has been created we need to add the following test. - -```js -describe("User can add a product to his/her order", () => { - beforeEach(() => { - cy.server(); - cy.route({ - method: "GET", - url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" - }); - - cy.route({ - method: "POST", - url: "http://localhost:3000/api/orders", - response: "fixture:post_response.json" - }); - - cy.route({ - method: "PUT", - url: "http://localhost:3000/api/orders/1", - response: "fixture:put_response.json" - }); - - cy.visit("http://localhost:3001"); - cy.get("#product-2").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - }); - cy.get("#product-3").within(() => { - cy.get("button") - .contains("Add to order") - .click(); - }); - cy.get("button") - .contains("View order") - .click(); - }); - - it("user can pay for his order", () => { - cy.get("button") - .contains("Confirm!") - .click(); - cy.get("#payment-form").should("exist"); - }); -}); -``` -So what is happening in this test? -Well, we are setting the stage and making sure that we can display a payment form. Remember that we always work in small chunks and that we continually refactor both our test but also our implementation code as we add more and more functionality. - -Let's head over to the component that we want to display our payment form. - -Start by adding a new state, where we are adding the payment form. Why are we setting it to false? - -```js -state = { - productData: [], - message: {}, - orderDetails: {}, - showOrder: false, - orderTotal: "", - showPaymentform: false -}; -``` - -After that, we need to refactor the button to show the form when the user clicks on it. - - -```js -; -{ - this.state.showPaymentform && ( -
    - -
    - ); -} -``` -remember to also import the on the top of the file. -But we don't have a PaymentForm component you are thinking. Well, that's right. We don't so... -Let's create one. - -```bash -$ touch src/components/PaymentForm.jsx -``` - -```js -import React, { Component } from "react"; - -class PaymentForm extends Component { - render() { - return ( -
    -

    Here we will show a payment form

    -
    - ); - } -} - -export default PaymentForm; -``` -Run your test, commit and all that fun stuff. -Just to check, you did remember to create a new branch on git before we started right? Of course you did. silly me.... - -Let's revisit our test for one moment. When we are dealing with stripe they provide us with a lot of functionality in terms of security and checks. This is good for development but could be an issue when we are trying to test the payment functionality. We need to test that we can see an iframe where we will be displaying the payment form. - -Refactor your test to look like this. - -```js -it("user can pay for his order", () => { - cy.get("button") - .contains("Confirm!") - .click(); - cy.get("#payment-form").should("exist"); - cy.wait(1000); - cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { - const $body = $iframe.contents().find("body"); - cy.wrap($body) - .find('input[name="cardnumber"]') - .type("4242424242424242", { delay: 50 }); - }); -}); -``` - -We are finding an input field and filling it out with the card number. Small steps remember. Run your test and behold the error messages. - -Time for the implementation - -Add the following package. - -```bash -$ yarn add react-stripe-elements -``` - -To use stripe we also need to add a script provided by stripe. - -Add a script to the head in index.html - -```html - -``` - -And we also need to turn off some of the built-in settings on cypress just to make sure that we are not getting errors because of our browser. - -Setting chromeWebSecurity to false in Chrome-based browsers allows you to do the following: - -- Display insecure content -- Navigate to any superdomain without cross-origin errors -- Access cross-origin iframes that are embedded in your application - -Add `chromeWebSecurity` and set it to `false` - -`cypress.json` - -```json -{ - "baseUrl": "http://localhost:3001", - "chromeWebSecurity": false -} -``` -Head over to your `index.js` and import the StripeProvider. - -```js -import { StripeProvider } from "react-stripe-elements"; -``` - -And wrap component with the StripeProvider - -```js -ReactDOM.render( - - - , - document.getElementById("root") -); -``` - -The StripeProvider gives us access to the Stripe object. The Stripe object will contain the secret, and publishable key which will allow us to access the Stripe API. - -Get your API key from the stripe dashboard, make sure that you select "Show test data" and go to the developer's tab. Copy your Publishable key and add it to the stripe provider in your App component. - -```js -ReactDOM.render( - - - , - document.getElementById("root") -); -``` -Time to start working on the payment form itself. -import - -Wrap the PaymentForm component with the Elements, and make sure that you import Elements from `"react-stripe-elements"` - -```js -{ - this.state.showPaymentform && ( -
    - - - -
    - ); -} -``` - -Move over to your PaymentForm and update it with the following code. - -```js -import React, { Component } from "react"; -import { injectStripe, CardNumberElement } from "react-stripe-elements"; - -class PaymentForm extends Component { - render() { - return ( - <> - - - - ); - } -} - -export default injectStripe(PaymentForm); -``` - -So what are we doing here? We are adding an input field where the users can add their card number but to use that we need to use the injectStripe component to make use of their form elements. - -Run the test now and they should be green. If they do, commit. - -So we have added a rudimentary payment form and we have also added the package that we need to use stripe in our react app. But we need multiple input fields for the rest of our card information. So let's add that now. - - - -We need to update our test again for the remaining input fields, as well as with the assertion that the payment has gone through. - -```js -it("user can pay for his order", () => { - cy.route({ - method: "PUT", - url: "http://localhost:3000/api/orders/1", - body: { activity: "finalize" }, - response: { paid: true, message: "Your order will be ready in 30 minutes!" } - }); - cy.get("button") - .contains("Confirm!") - .click(); - - cy.get("#payment-form").should("exist"); - cy.wait(1000); - cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { - const $body = $iframe.contents().find("body"); - cy.wrap($body) - .find('input[name="cardnumber"]') - .type("4242424242424242", { delay: 50 }); - }); - - cy.get('iframe[name^="__privateStripeFrame6"]').then($iframe => { - const $body = $iframe.contents().find("body"); - cy.wrap($body) - .find('input[name="exp-date"]') - .type("1222", { delay: 10 }); - }); - cy.get('iframe[name^="__privateStripeFrame7"]').then($iframe => { - const $body = $iframe.contents().find("body"); - cy.wrap($body) - .find('input[name="cvc"]') - .type("999", { delay: 10 }); - }); - - cy.get("button") - .contains("Submit") - .click(); - - cy.get("#payment-form").should("not.exist"); - cy.get(".message").should( - "contain", - "Your order will be ready in 30 minutes!" - ); -}); -``` - -Let us go back to your PaymentForm component and update the return with the rest of the payment fields. Make sure that you import the - -```js - return ( - <> - - - - - - - - -``` - -Next we want to add a function called payWithStripe - -```js -async payWithStripe() { - await this.props.stripe.createToken().then(response => { - if (response.token) { - try { - this.performPayment(response.token) - } - catch { - - } - } - - }) - } -``` - -We also want to add a function called performPayment. This function will make the payment. - -```js - async performPayment(token) { - let orderResponse = await axios.put( - `http://localhost:3000/api/orders/${this.props.orderDetails.id}`, - { - activity: 'finalize', - stripeToken: token - } - ) - if (orderResponse.data.paid === true) { - this.props.finalizeOrder(orderResponse.data.message) - - } else { - debugger - } - } -``` - -And finally, we need to update the button - -```js - -``` - -Make sure that you have made adequate imports to the component. - -Move over to the DisplayProduct component or your main component where you are adding the products to the order. - -Update the Props that you are sending through - -```js - -``` - -And update the finalizeOrder function we are passing in the finializeOrder function as a prop to the component. - -```js - async finalizeOrder(message) { - let orderTotal = this.state.orderDetails.order_total - this.setState({ message: { id: 0, message: message }, orderTotal: orderTotal, orderDetails: {}, showPaymentForm: false }) - } -``` - -Run your tests and they should go green. - -Let's move over to the backend - -## Stripe in the Backend - -Time to work on the backend. Make sure that you create a new branch here as well before you get started. - -Start with creating a request spec. - -```bash -$ touch spec/requests/api/client_can_send_payment_request_spec.rb -``` - -Add the following test - -```rb -RSpec.describe Api::OrdersController, type: :request do - let(:product_1) { create(:product, price: 50) } - let(:order) { create(:order) } - let!(:order_item) { create(:order_item, product: product_1, order: order) } - - before do - put "/api/orders/#{order.id}", - params: { activity: "finalize", - stripetoken: "12345", - email: 'testing@test.com' - } - end - - it '' do - expect(JSON.parse(response.body)).to eq JSON.parse('{"paid": true, "message": "Your order will be ready in 30 minutes!"}') - end -end -``` - -Run your tests and the should be failing. That is a good thing because now we know what to do next. - -Head over to your orders controller and add the following functionality to the update action - -```rb - def update - order = Order.find(params[:id]) - if params[:activity] - payment_status = perform_stripe_payment - if payment_status == true - # successful payment - order.update_attribute(:finalized, true) - render json: { paid: true, message: 'Your order will be ready in 30 minutes!' } - - else - # unsuccessful payment - end - - else - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: create_json_response(order) - end - end -``` -We are not dealing with the unsuccessful payment. Make sure that you add and test for an error message once we are done with the Happy path. - -You will get a Name error stating that there is no `perform_stripe_payment` - -Let's fix that. In the private section of the orders controller add the following method - -```rb - def perform_stripe_payment - order = Order.find(params[:id]) - customer = Stripe::Customer.create( - email: params[:email], - source: params[:stripeToken], - description: 'slowfood client' - ) - - charge = Stripe::Charge.create( - customer: customer.id, - amount: ordr.order_total, - currency: 'sek' - ) - charge - end -``` -We are calling the Stripe gem to create a customer but also to make charge the customer. - -You will be getting errors now, we need to fix that by adding the stripe gem to our Gemfile. - -`gem 'stripe-rails'` - -Run the tests again and your new error message should something in the lines of - -```bash - Stripe::AuthenticationError: - No API key provided. Set your API key using "Stripe.api_key = ". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions. -``` - -This means that we need to use our API key that Stripe has provided for us. - -Grab the "secret key" from your Stripe dashboard. - -Do not reveal this key. To use this securely we will add the key to our credentials file. Credentials is an encrypted file in rails. We need the master.key file in our config folder to open this file. The master.key is added to your gitignore by default so that you don\t accidentally push it to version control. - -Use the following steps to modify your credentials. - -Head over to your terminal and run the following command. - -```bash -$ EDITOR='code --wait' rails credentials:edit -``` -Notice that your terminal has been locked and the credentials file has been decrypted and opened in your code editor. The terminal will be locked until you save and close the credentials file in your code editor. But before we do that we need to add the secret key and publishable key in this file. - - -```yml -secret_key_base: 50fba2642c2978bcf7536a53606328149f18c7382a070822bf86f0d141095d9ed2c2c472b77577df08c34c61e36f3a27aaba26fb746cfb6a79ce3456edbcb04b -stripe: - pk_key: your_publishable_key_here - secret_key: your_secret_key_here -``` - -Save and close the file and you should be getting the following message in the terminal. - -```bash -16:16 $ EDITOR='code --wait' rails credentials:edit -File encrypted and saved. -``` - -To make use of the stripe credentials we need to add some configurations. - -Head over to the `application.rb` file where we turn off the generators etc and add the following configurations. - -```rb - config.generators do |generate| - generate.helper false - generate.assets false - generate.view_specs false - generate.helper_specs false - generate.routing_specs false - generate.controller_specs false - end - config.stripe.secret_key = Rails.application.credentials.stripe[:secret_key] - config.stripe.publishable_key = Rails.application.credentials.stripe[:pk_key] - end -``` - -We do not want to make actual network calls to stripe when we are in development mode. For that we will use a gem called `stripe-ruby-mock`. Make sure that you use the version stated below. - -Add the following gem to the Gemfile - -`gem 'stripe-ruby-mock', '~> 2.5.6', require: 'stripe_mock'` - -Then we want to update the rails_helper to add the mock functionality - -Add `require 'stripe_mock'` to the rails_helper and add the configuration to RSpec - -```rb -RSpec.configure do |config| - config.fixture_path = "#{::Rails.root}/spec/fixtures" - config.use_transactional_fixtures = true - config.infer_spec_type_from_file_location! - config.filter_rails_from_backtrace! - config.include FactoryBot::Syntax::Methods - config.include(Shoulda::Matchers::ActiveRecord, type: :model) - config.before(:each) do - @stripe_test_helper = StripeMock.create_test_helper - StripeMock.start - end - config.after(:each) do - StripeMock.stop - end -end -``` -Next up we want to modify the test with the mocked data. - -```rb - before do - put "/api/orders/#{order.id}", - params: { activity: "finalize", - stripeToken: StripeMock.create_test_helper.generate_card_token, - email:'user@mail.com'} - end -``` - -And finally update the perform_stripe_payment method in our controller. - -```rb - def perform_stripe_payment - order = Order.find(params[:id]) - customer = Stripe::Customer.create( - email: params[:email], - source: params[:stripeToken], - description: 'slowfood client' - ) - - charge = Stripe::Charge.create( - customer: customer.id, - amount: order.order_total.to_i * 100, - currency: 'sek' - ) - charge.paid - end - ``` - - Run your tests now and they should be going green. - So Stripe added. - Let's celebrate by making a commit! - From bc19407e294171df7844e040bcdbbbadf0386809 Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Wed, 8 Apr 2020 08:19:12 +0200 Subject: [PATCH 17/19] Adds introduction section --- .../react_checkout_guide/00_introduction.md | 19 +++++++++++++++++++ .../01_getting_started.md | 14 +------------- .../19_finalizing_the_payment.md | 14 ++++++-------- 3 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 guides/react_checkout_guide/00_introduction.md diff --git a/guides/react_checkout_guide/00_introduction.md b/guides/react_checkout_guide/00_introduction.md new file mode 100644 index 00000000..88b7dd96 --- /dev/null +++ b/guides/react_checkout_guide/00_introduction.md @@ -0,0 +1,19 @@ +# Adding Checkout functionality to your React application + +``` +This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. +If there are typos or issues in this guide, please notify a coach +``` + +In this section we are going to add checkout functionality to our react application. +The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. + +Start with making sure that you have the latest code pulled from GitHub on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. + +As always we will work in a test-driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. + +If you want to only learn about how to create a checkout functionality in react you can clone down the following repos: +[API](https://github.com/CraftAcademy/slowfood_api_checkout_boilerplate) & +[Client](https://github.com/CraftAcademy/slowfood_client_checkout_boilerplate) + +Let's gets started \ No newline at end of file diff --git a/guides/react_checkout_guide/01_getting_started.md b/guides/react_checkout_guide/01_getting_started.md index 18cfc8ad..55692d70 100644 --- a/guides/react_checkout_guide/01_getting_started.md +++ b/guides/react_checkout_guide/01_getting_started.md @@ -1,16 +1,4 @@ -# Adding Checkout functionality to your React application - -``` -This guide is based on a demo that we ran a while back. Please take note that there might be issues for you when adding this functionality as your implementation probably looks different. This is also the first iteration of the guide and updates will be made in the future. -If there are typos or issues in this guide, please notify a coach -``` - -In this section we are going to add checkout functionality to our react application. -The prerequisite for this functionality is that we have products on our page that are ready to sell. Please remember as we implement we will omit explaining code that has already been covered. Remember to commit often in case you need to revert back and not lose large sections of the implementation. - -Start with making sure that you have the latest code pulled from GitHub on your development branch and create a new branch called `add_order_functionality` or something that you think is appropriate to the user story. - -As always we will work in a test-driven way. Making sure that we let our acceptance test guide our development. This will ensure us that we are not biting off more than we can chew. But also that we stay in scope. +## Getting started Create a new feature file by typing `$ touch cypress/integration/userCanAddProductsToOrder.feature.js` diff --git a/guides/react_checkout_guide/19_finalizing_the_payment.md b/guides/react_checkout_guide/19_finalizing_the_payment.md index f06bc082..49e7c29e 100644 --- a/guides/react_checkout_guide/19_finalizing_the_payment.md +++ b/guides/react_checkout_guide/19_finalizing_the_payment.md @@ -1,6 +1,6 @@ ## Finalizing the payment -Next up we want to modify the test with the mocked data. +Next up we want to modify the test with the mocked data. ```rb before do @@ -11,7 +11,7 @@ Next up we want to modify the test with the mocked data. end ``` -And finally update the perform_stripe_payment method in our controller. +And finally update the perform_stripe_payment method in our controller. ```rb def perform_stripe_payment @@ -26,12 +26,10 @@ And finally update the perform_stripe_payment method in our controller. customer: customer.id, amount: order.order_total.to_i * 100, currency: 'sek' - ) + ) charge.paid end - ``` - - Run your tests now and they should be going green. - So Stripe added. - Let's celebrate by making a commit! +``` +Run your tests now and they should be going green. So Stripe added. +Let's celebrate by making a commit! From a2115a9a2e86200b06cb600c1829dcae6d8bb12d Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Thu, 9 Apr 2020 07:11:59 +0200 Subject: [PATCH 18/19] resolves issues in the react checkout --- .../01_getting_started.md | 32 +++++----- .../02_displaying_messages_and_the_backend.md | 16 +++-- .../03_the_orders_model.md | 1 - .../react_checkout_guide/04_some_updates.md | 46 ++++++--------- ...eter.md => 05_adding_multiple_products.md} | 56 +++++++----------- .../06_viewing_the_order.md | 59 ++++--------------- .../07_some_refactoring.md | 10 ++-- guides/react_checkout_guide/08_serializers.md | 13 +++- .../09_back_to_the_frontend.md | 33 ++++++----- .../10_finalizing_the_order_on_the_backend.md | 20 +++---- .../11_finializing_on_the_client_side.md | 49 ++++++++------- .../13_setting_the_stage.md | 2 +- 12 files changed, 145 insertions(+), 192 deletions(-) rename guides/react_checkout_guide/{05_putting_it_togheter.md => 05_adding_multiple_products.md} (79%) diff --git a/guides/react_checkout_guide/01_getting_started.md b/guides/react_checkout_guide/01_getting_started.md index 55692d70..193f3833 100644 --- a/guides/react_checkout_guide/01_getting_started.md +++ b/guides/react_checkout_guide/01_getting_started.md @@ -12,22 +12,20 @@ describe("User can add a product to their order", () => { cy.route({ method: "GET", url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" + response: "fixture:product_data.json", }); cy.route({ method: "POST", url: "http://localhost:3000/api/orders", - response: { message: "A product has been added to your order" } + response: { message: "A product has been added to your order" }, }); }); it("user gests a confirmation messsage when adding a a producut to order", () => { cy.visit("/"); cy.get("#product-1").within(() => { - cy.get("button") - .contains("Add to order") - .click(); + cy.get("button").contains("Add to order").click(); }); cy.contains("A product has been added to your order"); }); @@ -42,12 +40,12 @@ In our case we already have a component that we use to display the products, all Let's add the implementation code. -We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are mapping over the products. +We need to start with the first step which is to make sure that we have a button available to click on, let's add that where we are mapping over the products, for example `DisplayProductData` ```jsx dataIndex = (
    - {this.state.productData.map(item => { + {this.state.productData.map((item) => { return (
    {`${item.name} ${item.description} ${item.price}`} @@ -99,15 +97,17 @@ Add the dataset to the parent div: Like this: ```js -
    - {`${item.name} ${item.description} ${item.price}`} - -
    +return ( +
    + {`${item.name} ${item.description} ${item.price}`} + +
    +); ``` Now if you run your test, the debugger should kick in, run the following command in the console `event.target.parentElement.dataset`. diff --git a/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md b/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md index 10a398ec..b672f21e 100644 --- a/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md +++ b/guides/react_checkout_guide/02_displaying_messages_and_the_backend.md @@ -6,7 +6,7 @@ Let's start with adding the message to the state: ```js state = { productData: [], - message: {} + message: {}, }; ``` @@ -15,7 +15,8 @@ Next, we need to display the message below the button. ```js [....] - {parseInt(this.state.message.id) === item.id &&

    {this.state.message.message}

    } +{parseInt(this.state.message.id) === item.id && +

    {this.state.message.message}

    } ``` Finally, we need to set the new state in the `addToOrder` function @@ -27,9 +28,10 @@ async addToOrder(event) { this.setState({message: {id: id, message: result.data.message}}) } ``` + If we run our tests now everything should go green. -Let's move over to the backend and start with creating a spec that we are going to use. Make sure that you create a new branch to work on. +Let's move over to the backend and work on our API. We will start with creating a spec. Make sure that you create a new branch to work on. ```bash $ touch spec/requests/api/client_can_create_new_order_spec.rb @@ -86,5 +88,9 @@ class Api::OrdersController < ApplicationController end end ``` - -Cool. let's move over to the model. \ No newline at end of file +We are getting a new error: +```bash +NameError: uninitialized constant Api::OrdersController::Order +``` +To fix this we need to create a new model +Cool. let's move over to the model. diff --git a/guides/react_checkout_guide/03_the_orders_model.md b/guides/react_checkout_guide/03_the_orders_model.md index 5cb5d3e7..70e61323 100644 --- a/guides/react_checkout_guide/03_the_orders_model.md +++ b/guides/react_checkout_guide/03_the_orders_model.md @@ -56,4 +56,3 @@ Run your migrations and then run your specs. Everything should be green like Bru ![the hulk is happy](https://media.giphy.com/media/i3lbNZhnB1Jle/giphy.gif) -Done here and working \ No newline at end of file diff --git a/guides/react_checkout_guide/04_some_updates.md b/guides/react_checkout_guide/04_some_updates.md index 840caafd..fd5d7567 100644 --- a/guides/react_checkout_guide/04_some_updates.md +++ b/guides/react_checkout_guide/04_some_updates.md @@ -2,51 +2,39 @@ We have added a lot of functionality and e need to revisit our specs and make some updates to have it work on the client-side later on. -First, we need to amend our request spec. +First, we need to amend our request spec `client_can_create_new_order_spec`. We will amend our specs by adding a `before` block ```ruby - before do - post '/api/orders', params: { id: product_1.id } - @order = Order.last - end +before do + post '/api/orders', params: { id: product_1.id } + @order = Order.last +end ``` And adding a new spec ```ruby - it 'adds another product to order if param "order_id" is present' do - post '/api/orders', params {id: product_2.id, order_id: @order.id} - expect(@order.order_items.count).to eq 2 - end +it 'adds product to order if param "order_id" is present' do + post '/api/orders', params: {id: product_2.id, order_id: @order.id} + expect(@order.order_items.count).to eq 2 +end ``` -To get this to work in the first iteration we can simply refactor our create action in the controller. +And ```ruby -class Api::OrdersController < ApplicationController - def create - order = if params[:order_id] - Order.find(params[:order_id]) - else - Order.create - order.order_items.create(product_id: params[:product_id]) - render json: { message: 'The product has been added to your order', order_id: order.id } - end +it 'adds another product to order if param "order_id" is present' do + put "/api/orders/#{order.id}", params: {product_id: product_2.id } + expect(@order.order_items.count).to eq 2 end ``` -Run your test and make sure that this is going green. It is working however this is not the best practice. Let's make sure that we follow the conventions and refactor this code so it works. Let's refactor our test first -```ruby - it 'adds another product to order if param "order_id" is present' do - put "/api/orders/#{order.id}", params: {product_id: product_2.id } - expect(@order.order_items.count).to eq 2 - end -``` -You will probably get a routing error. We will fix that with adding update to the routes: + +You will probably get a routing error. We will fix that with adding update & create to the routes: `resources :orders, only: [:create, :update]` @@ -62,7 +50,7 @@ Run the test again and you will get a error stating that there is no action `upd end ``` -And remember to clean out the create action to its previous state: +And the create action: ```ruby def create @@ -72,7 +60,7 @@ And remember to clean out the create action to its previous state: end ``` -We will have to refactor our test to better suit our implementation code. The final shape of the test will look like this. +We will have to refactor our test to better suit our implementation code, dy dividing both functionalities in out tests. The final shape of the test will look like this. ```rb RSpec.describe Api::OrdersController, type: :request do diff --git a/guides/react_checkout_guide/05_putting_it_togheter.md b/guides/react_checkout_guide/05_adding_multiple_products.md similarity index 79% rename from guides/react_checkout_guide/05_putting_it_togheter.md rename to guides/react_checkout_guide/05_adding_multiple_products.md index 5789c981..cc6c20dd 100644 --- a/guides/react_checkout_guide/05_putting_it_togheter.md +++ b/guides/react_checkout_guide/05_adding_multiple_products.md @@ -1,15 +1,3 @@ -### Putting it together - -For the client to work with the backend we need to make some adjustment to the code. In your client make sure that you add product_id instead of the id. - -```js -async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - this.setState({message: {id: id, message: result.data.message}}) -} -``` - ## Adding multiple products to the order Let's add the functionality to add multiple products to our order. As always we need to have a feature test for that. We will add this to the test that we already have. @@ -28,7 +16,7 @@ describe("User can add a product to his/her order", () => { cy.route({ method: "GET", url: "http://localhost:3000/api/products", - response: "fixture:product_data.json" + response: "fixture:product_data.json", }); cy.route({ @@ -36,8 +24,8 @@ describe("User can add a product to his/her order", () => { url: "http://localhost:3000/api/orders", response: { message: "The product has been added to your order", - order_id: 1 - } + order_id: 1, + }, }); cy.route({ @@ -45,17 +33,15 @@ describe("User can add a product to his/her order", () => { url: "http://localhost:3000/api/orders/1", response: { message: "The product has been added to your order", - order_id: 1 - } + order_id: 1, + }, }); cy.visit("http://localhost:3001"); }); it("user get a confirmation message when adding product to order", () => { cy.get("#product-2").within(() => { - cy.get("button") - .contains("Add to order") - .click(); + cy.get("button").contains("Add to order").click(); cy.get(".message").should( "contain", "The product has been added to your order" @@ -63,9 +49,7 @@ describe("User can add a product to his/her order", () => { }); cy.get("#product-3").within(() => { - cy.get("button") - .contains("Add to order") - .click(); + cy.get("button").contains("Add to order").click(); cy.get(".message").should( "contain", "The product has been added to your order" @@ -85,7 +69,7 @@ Add the `orderId` to the state: state = { productData: [], message: {}, - orderId: "" + orderId: "", }; ``` @@ -104,16 +88,16 @@ The `order_id` is what is being returned to us from the backend. We also need to make sure that we are adding an order to an existing one, if one already exists so let's add a conditional for that. ```js - async addToOrder(event) { - let id = event.target.parentElement.dataset.id - let result - if (this.state.orderId !== "") { - result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) - } else { - result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) - } - this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) - } +async addToOrder(event) { + let id = event.target.parentElement.dataset.id + let result + if (this.state.orderId !== "") { + result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderId}`, { product_id: id }) + } else { + result = await axios.post('http://localhost:3000/api/orders', { product_id: id } ) + } + this.setState({message: {id: id, message: result.data.message}, orderId: result.data.order_id}) +} ``` So what does this conditional do? We are simply stating that if there is an `order` and that the `orderId` is not empty, then add the new product to that order with a `PUT` request. Otherwise, go ahead and create a new order. @@ -126,7 +110,7 @@ We want to make sure that we can view the products that we have added to our ord First, we want to make sure that there is a button where we can view the order. However, this button should only be visible when we have added products to our order. -Refactor your code to look like this: +As a best practice, we will go back to our tests and refactor them a little. Refactor your code to look like this: ```js [....] @@ -158,4 +142,4 @@ return ( ); ``` -Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. \ No newline at end of file +Run your tests now again and they should go green. Remember that we are only displaying the button, it has no functionality yet. diff --git a/guides/react_checkout_guide/06_viewing_the_order.md b/guides/react_checkout_guide/06_viewing_the_order.md index 1009c726..289a5975 100644 --- a/guides/react_checkout_guide/06_viewing_the_order.md +++ b/guides/react_checkout_guide/06_viewing_the_order.md @@ -6,53 +6,6 @@ Now it's time for adding the view order functionality to the button. As always we start with the test. -```js -cy.get("button") - .contains("View order") - .click(); -cy.get("#order-details").within(() => { - cy.get("li").should("have.length", 2); -}); - -cy.get("button") - .contains("View order") - .click(); -cy.get("#order-details").should("not.exist"); -``` - -And now for the implementation code. First we update our state object - -```js -state = { - productData: [], - message: {}, - orderId: "", - showOrder: false -}; -``` - -And then the button - -```js -return ( - <> - {this.state.orderDetails.hasOwnProperty('products') && - - } - {this.state.showOrder && -
      - {orderDetailsDisplay} -
    - } - {dataIndex} - -``` - - -Let's work on the view order - -Go to the client, first we will write the test - ```js it("user can add multiple product to order and view its content", () => { cy.get("button") @@ -97,6 +50,18 @@ it("user can add multiple product to order and view its content", () => { ``` Run the test and it is failing.... + +And now for the implementation code. First we update our state object + +```js +state = { + productData: [], + message: {}, + orderId: "", + showOrder: false +}; +``` + Add the following code to the return block of your component ```js diff --git a/guides/react_checkout_guide/07_some_refactoring.md b/guides/react_checkout_guide/07_some_refactoring.md index 7682aa7e..904e2e84 100644 --- a/guides/react_checkout_guide/07_some_refactoring.md +++ b/guides/react_checkout_guide/07_some_refactoring.md @@ -63,19 +63,19 @@ describe("User can add a product to his/her order", () => { cy.route({ method: "GET", url: "http://localhost:3000/api/products", - response: "fixture:product_data.json", + response: "fixture:product_data.json", }); cy.route({ method: "POST", url: "http://localhost:3000/api/orders", - response: "fixture:post_response.json", + response: "fixture:post_response.json", // use the fixture here }); cy.route({ method: "PUT", url: "http://localhost:3000/api/orders/1", - response: "fixture:put_response.json", + response: "fixture:put_response.json", // use the fixture here }); cy.visit("http://localhost:3001"); }); @@ -115,7 +115,7 @@ describe("User can add a product to his/her order", () => { ``` And finally our implementation code: -It should look like this ater you have refactored it. +It should look like this after you have refactored it. ```js import React, { Component } from "react"; @@ -187,6 +187,7 @@ class DisplayProductData extends Component {
    ); } + if (this.state.orderDetails.hasOwnProperty("products")) { orderDetailsDisplay = this.state.orderDetails.products.map((item) => { return
  • {item.name}
  • ; @@ -214,5 +215,6 @@ class DisplayProductData extends Component { } export default DisplayProductData; ``` +Your code might look different at this stage, if you are using different naming for the components. But this is how ours look. Take a moment to go over your code. Are there any further rooms for improvements? ask yourself if we can divide the code into more components or perhaps modules? Make sure that you note potential improvments that you and your team could make here. Run the test and move to the next section. diff --git a/guides/react_checkout_guide/08_serializers.md b/guides/react_checkout_guide/08_serializers.md index 087ce92a..00fda581 100644 --- a/guides/react_checkout_guide/08_serializers.md +++ b/guides/react_checkout_guide/08_serializers.md @@ -92,7 +92,7 @@ RSpec.describe Api::OrdersController, type: :request do end ``` -And now for the serializer +And now for the serializer. Go over the code here carefully and try to understand what is happening. I advise you to use binding.pry to stop the execution of the code and dive deeper into hoe these different methods work together. `app/serializers/order_serializer.rb` @@ -138,7 +138,7 @@ class OrderSerializer < ActiveModel::Serializer end ``` -And finally we need to update our controller with the changes. +And we need to update our controller with the changes. `app/controllers/api/orders_controller.rb` @@ -165,3 +165,12 @@ private end end ``` + +And finally add this the method to order model. +```rb +def order_total + order_items.joins(:product).sum("products.price") +end +``` + +Run your tests \ No newline at end of file diff --git a/guides/react_checkout_guide/09_back_to_the_frontend.md b/guides/react_checkout_guide/09_back_to_the_frontend.md index 1fd922a8..273139b6 100644 --- a/guides/react_checkout_guide/09_back_to_the_frontend.md +++ b/guides/react_checkout_guide/09_back_to_the_frontend.md @@ -2,20 +2,21 @@ Now we want to go back to our frontend and add the changes there as well. We need to make sure that the order totals are showing up. -As always we first start with our tests. +As always we first start with our tests. Refactor your test by adding the following section to your it block in your test file. ```js -cy.get("button") - .contains("View order") - .click(); -cy.get("#order-details").within(() => { - cy.get("li") - .should("have.length", 2) - .first() - .should("have.text", "1 x Salad") - .next() - .should("have.text", "1 x Ice Cream"); -}); +[....] +cy.get('button').contains('View order').click() + +cy.get('#order-details').within(() => { + cy.get('li') + .should('have.length', 2) + .first().should('have.text', '1 x Salad') + .next().should('have.text', '1 x Ice Cream') +}) + +cy.get('button').contains('View order').click() +cy.get('#order-details').should('not.exist') ``` We also need to import the `DisplayProductData` to the feature file. @@ -89,10 +90,12 @@ So the function looks like this this.setState({ message: { id: id, message: result.data.message }, orderDetails: result.data.order }) ``` -And in our render function +And in our if statement in the orderDetailDisplay ```js -return
  • {`${item.amount} x ${item.name}`}
  • ; +orderDetailsDisplay = this.state.orderDetails.products.map((item) => { + return
  • {`${item.amount} x ${item.name}`}
  • ; +}); ``` And finally in the return @@ -115,4 +118,4 @@ return ( ``` -Commit this and let's go back to the backend to finalize the order. \ No newline at end of file +Commit this and let's go back to the backend to finalize the order. diff --git a/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md b/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md index 27e3b058..70321b25 100644 --- a/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md +++ b/guides/react_checkout_guide/10_finalizing_the_order_on_the_backend.md @@ -53,17 +53,17 @@ Run the migrations and make sure that the changes have been added to your schema `app/controllers/api/orders_controller.rb` ```rb - def update - order = Order.find(params[:id]) - if params[:activity] - order.update_attribute(:finalized, true) - render json: { message: 'Your order will be ready in 30 minutes!' } - else - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: create_json_response(order) - end +def update + order = Order.find(params[:id]) + if params[:activity] + order.update_attribute(:finalized, true) + render json: { message: 'Your order will be ready in 30 minutes!' } + else + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) end +end ``` Remember that need to update our order serializer as well. diff --git a/guides/react_checkout_guide/11_finializing_on_the_client_side.md b/guides/react_checkout_guide/11_finializing_on_the_client_side.md index eced6c0e..2445f9a6 100644 --- a/guides/react_checkout_guide/11_finializing_on_the_client_side.md +++ b/guides/react_checkout_guide/11_finializing_on_the_client_side.md @@ -1,33 +1,25 @@ ## Finalizing and back to the client Okay...The final stretch -As always let's start with a new spec. +As always let's start with a new test. Add the following code to your feature test: ```js it("user can finalize the order", () => { cy.get("#product-2").within(() => { - cy.get("button") - .contains("Add to order") - .click(); + cy.get("button").contains("Add to order").click(); }); cy.get("#product-3").within(() => { - cy.get("button") - .contains("Add to order") - .click(); + cy.get("button").contains("Add to order").click(); }); - cy.get("button") - .contains("View order") - .click(); + cy.get("button").contains("View order").click(); cy.route({ method: "PUT", url: "http://localhost:3000/api/orders/1", - response: { message: "Your order will be ready in 30 minutes!" } + response: { message: "Your order will be ready in 30 minutes!" }, }); - cy.get("button") - .contains("Confirm!") - .click(); + cy.get("button").contains("Confirm!").click(); cy.get(".message").should( "contain", "Your order will be ready in 30 minutes!" @@ -36,6 +28,7 @@ it("user can finalize the order", () => { ``` We also want to make sure that we have som configuration to use in the cypress tests. We are adding a delay depending on the different functions that cypress runs. + ```js // cypress/support/commands.js const COMMAND_DELAY = 150; @@ -48,12 +41,12 @@ for (const command of [ "type", "clear", "reload", - "contains" + "contains", ]) { Cypress.Commands.overwrite(command, (originalFn, ...args) => { const origVal = originalFn(...args); - return new Promise(resolve => { + return new Promise((resolve) => { setTimeout(() => { resolve(origVal); }, COMMAND_DELAY); @@ -62,7 +55,7 @@ for (const command of [ } ``` -Now for the implementation code: +Let's work on the implementation code: Update the state object with orderTotal @@ -72,18 +65,18 @@ state = { message: {}, orderDetails: {}, showOrder: false, - orderTotal: "" + orderTotal: "", }; ``` Add a new asynchronous function to handle the finalize order ```js - async finalizeOrder() { - let orderTotal = this.state.orderDetails.order_total - let result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { activity: 'finalize' }) - this.setState({ message: { id: 0, message: result.data.message }, orderTotal: orderTotal, orderDetails: {}}) - } +async finalizeOrder() { + let orderTotal = this.state.orderDetails.order_total + let result = await axios.put(`http://localhost:3000/api/orders/${this.state.orderDetails.id}`, { activity: 'finalize' }) + this.setState({ message: { id: 0, message: result.data.message }, orderTotal: orderTotal, orderDetails: {}}) +} ``` You also need to update the `addToOrder` function @@ -125,11 +118,15 @@ return ( And finally the render function: ```js -{ - `${item.name} ${item.description} - ${item.price}kr `; +if (this.state.orderDetails.hasOwnProperty("products")) { + orderDetailsDisplay = this.state.orderDetails.products.map((item) => { + return
  • {`${item.amount} x ${item.name}`}
  • ; + }); +} else { + orderDetailsDisplay = "Nothing to see"; } ``` -Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. Go over your code again and marvel at your creation. This is one approach we can take to create an ordering functionality in React from scratch. +Run your tests now and they should be green. We have now finalized the entire creating order functionality both in the front and the backend. Go over your code again and marvel at your creation. This is one approach we can take to create an ordering functionality in React from scratch. The last thing we need to do is to add our payment gateway. diff --git a/guides/react_checkout_guide/13_setting_the_stage.md b/guides/react_checkout_guide/13_setting_the_stage.md index b5004780..ed9c9377 100644 --- a/guides/react_checkout_guide/13_setting_the_stage.md +++ b/guides/react_checkout_guide/13_setting_the_stage.md @@ -28,7 +28,7 @@ After that, we need to refactor the button to show the form when the user clicks ); } ``` -Remember to also import the on the top of the file. +Remember to also import the on the top of the file. But we don't have a PaymentForm component you are thinking. Well, that's right. We don't so... Let's create one. From 06ed5404cb7a9235afd76ac3d2238f9c03faa527 Mon Sep 17 00:00:00 2001 From: faraznaeem Date: Thu, 9 Apr 2020 07:24:41 +0200 Subject: [PATCH 19/19] Finalizes the guide --- .../13_setting_the_stage.md | 6 +- .../15_the_payment_form.md | 4 +- .../16_adding_multiple_input_field.md | 96 +++++++++---------- .../17_stripe_in_the_backend.md | 70 +++++++------- .../18_adding_credentials.md | 2 +- .../19_finalizing_the_payment.md | 40 ++++---- 6 files changed, 106 insertions(+), 112 deletions(-) diff --git a/guides/react_checkout_guide/13_setting_the_stage.md b/guides/react_checkout_guide/13_setting_the_stage.md index ed9c9377..4a3a9ce2 100644 --- a/guides/react_checkout_guide/13_setting_the_stage.md +++ b/guides/react_checkout_guide/13_setting_the_stage.md @@ -9,7 +9,7 @@ state = { orderDetails: {}, showOrder: false, orderTotal: "", - showPaymentform: false + showPaymentForm: false }; ``` @@ -17,11 +17,11 @@ After that, we need to refactor the button to show the form when the user clicks ```js -; { - this.state.showPaymentform && ( + this.state.showPaymentForm && (
    diff --git a/guides/react_checkout_guide/15_the_payment_form.md b/guides/react_checkout_guide/15_the_payment_form.md index 6bde7f18..c21b1307 100644 --- a/guides/react_checkout_guide/15_the_payment_form.md +++ b/guides/react_checkout_guide/15_the_payment_form.md @@ -4,7 +4,7 @@ Wrap the PaymentForm component with the Elements, and make sure that you import ```js { - this.state.showPaymentform && ( + this.state.showPaymentForm && (
    @@ -36,7 +36,5 @@ export default injectStripe(PaymentForm); So what are we doing here? We are adding an input field where the users can add their card number but to use that we need to use the injectStripe component to make use of their form elements. -Run the test now and they should be green. If they do, commit. - So we have added a rudimentary payment form and we have also added the package that we need to use stripe in our react app. But we need multiple input fields for the rest of our card information. So let's add that now. diff --git a/guides/react_checkout_guide/16_adding_multiple_input_field.md b/guides/react_checkout_guide/16_adding_multiple_input_field.md index 2a5d845c..4a48bf7c 100644 --- a/guides/react_checkout_guide/16_adding_multiple_input_field.md +++ b/guides/react_checkout_guide/16_adding_multiple_input_field.md @@ -1,6 +1,6 @@ ## Adding multiple input fields in the payment form -We need to update our test again for the remaining input fields, as well as with the assertion that the payment has gone through. +We need to update our test again for the remaining input fields, as well as with the assertion that the payment has gone through. ```js it("user can pay for his order", () => { @@ -8,37 +8,32 @@ it("user can pay for his order", () => { method: "PUT", url: "http://localhost:3000/api/orders/1", body: { activity: "finalize" }, - response: { paid: true, message: "Your order will be ready in 30 minutes!" } + response: { + paid: true, + message: "Your order will be ready in 30 minutes!", + }, }); - cy.get("button") - .contains("Confirm!") - .click(); + cy.get("button").contains("Confirm!").click(); cy.get("#payment-form").should("exist"); cy.wait(1000); - cy.get('iframe[name^="__privateStripeFrame5"]').then($iframe => { + cy.get('iframe[name^="__privateStripeFrame5"]').then(($iframe) => { const $body = $iframe.contents().find("body"); cy.wrap($body) .find('input[name="cardnumber"]') .type("4242424242424242", { delay: 50 }); }); - cy.get('iframe[name^="__privateStripeFrame6"]').then($iframe => { + cy.get('iframe[name^="__privateStripeFrame6"]').then(($iframe) => { const $body = $iframe.contents().find("body"); - cy.wrap($body) - .find('input[name="exp-date"]') - .type("1222", { delay: 10 }); + cy.wrap($body).find('input[name="exp-date"]').type("1222", { delay: 10 }); }); - cy.get('iframe[name^="__privateStripeFrame7"]').then($iframe => { + cy.get('iframe[name^="__privateStripeFrame7"]').then(($iframe) => { const $body = $iframe.contents().find("body"); - cy.wrap($body) - .find('input[name="cvc"]') - .type("999", { delay: 10 }); + cy.wrap($body).find('input[name="cvc"]').type("999", { delay: 10 }); }); - cy.get("button") - .contains("Submit") - .click(); + cy.get("button").contains("Submit").click(); cy.get("#payment-form").should("not.exist"); cy.get(".message").should( @@ -48,22 +43,24 @@ it("user can pay for his order", () => { }); ``` -Let us go back to your PaymentForm component and update the return with the rest of the payment fields. Make sure that you import the +We are adding multiple tests for the different iframes that stripe provides us with, the "credit card numbers" we are passing are given to us by stripe for testing the functionality. + +Let us go back to your PaymentForm component and update the return with the rest of the payment fields. Make sure that you import all the stripe components ```js - return ( - <> - - - - - - - - +return ( + <> + + + + + + + + ``` -Next we want to add a function called `payWithStripe` +Next we want to add a function called `payWithStripe` in the component. ```js async payWithStripe() { @@ -84,21 +81,21 @@ async payWithStripe() { We also want to add a function called performPayment. This function will make the payment. ```js - async performPayment(token) { - let orderResponse = await axios.put( - `http://localhost:3000/api/orders/${this.props.orderDetails.id}`, - { - activity: 'finalize', - stripeToken: token - } - ) - if (orderResponse.data.paid === true) { - this.props.finalizeOrder(orderResponse.data.message) - - } else { - debugger - } - } +async performPayment(token) { + let orderResponse = await axios.put( + `http://localhost:3000/api/orders/${this.props.orderDetails.id}`, + { + activity: 'finalize', + stripeToken: token + } + ) + if (orderResponse.data.paid === true) { + this.props.finalizeOrder(orderResponse.data.message) + + } else { + debugger + } +} ``` And finally, we need to update the button @@ -123,13 +120,12 @@ Update the Props that you are sending through And update the finalizeOrder function we are passing in the finializeOrder function as a prop to the component. ```js - async finalizeOrder(message) { - let orderTotal = this.state.orderDetails.order_total - this.setState({ message: { id: 0, message: message }, orderTotal: orderTotal, orderDetails: {}, showPaymentForm: false }) - } +async finalizeOrder(message) { + let orderTotal = this.state.orderDetails.order_total + this.setState({ message: { id: 0, message: message }, orderTotal: orderTotal, orderDetails: {}, showPaymentForm: false }) +} ``` Run your tests and they should go green. -Let's move over to the backend - +Let's move over to the backend diff --git a/guides/react_checkout_guide/17_stripe_in_the_backend.md b/guides/react_checkout_guide/17_stripe_in_the_backend.md index c895fa84..e87d08ac 100644 --- a/guides/react_checkout_guide/17_stripe_in_the_backend.md +++ b/guides/react_checkout_guide/17_stripe_in_the_backend.md @@ -1,6 +1,6 @@ ## Stripe in the Backend -Time to work on the backend. Make sure that you create a new branch here as well before you get started. +Time to work on the backend. Make sure that you create a new branch here as well before you get started. Start with creating a request spec. @@ -24,7 +24,7 @@ RSpec.describe Api::OrdersController, type: :request do } end - it '' do + it 'user can pay for the order' do expect(JSON.parse(response.body)).to eq JSON.parse('{"paid": true, "message": "Your order will be ready in 30 minutes!"}') end end @@ -35,26 +35,27 @@ Run your tests and the should be failing. That is a good thing because now we kn Head over to your orders controller and add the following functionality to the update action ```rb - def update - order = Order.find(params[:id]) - if params[:activity] - payment_status = perform_stripe_payment - if payment_status == true +def update + order = Order.find(params[:id]) + if params[:activity] + payment_status = perform_stripe_payment + + if payment_status == true # successful payment order.update_attribute(:finalized, true) render json: { paid: true, message: 'Your order will be ready in 30 minutes!' } - - else - # unsuccessful payment - end - else - product = Product.find(params[:product_id]) - order.order_items.create(product: product) - render json: create_json_response(order) + # unsuccessful payment end + + else + product = Product.find(params[:product_id]) + order.order_items.create(product: product) + render json: create_json_response(order) end +end ``` + We are not dealing with the unsuccessful payment. Make sure that you add and test for an error message once we are done with the Happy path. You will get a Name error stating that there is no `perform_stripe_payment` @@ -62,35 +63,34 @@ You will get a Name error stating that there is no `perform_stripe_payment` Let's fix that. In the private section of the `orders` controller add the following method ```rb - def perform_stripe_payment - order = Order.find(params[:id]) - customer = Stripe::Customer.create( - email: params[:email], - source: params[:stripeToken], - description: 'slowfood client' - ) - - charge = Stripe::Charge.create( - customer: customer.id, - amount: ordr.order_total, - currency: 'sek' +def perform_stripe_payment + order = Order.find(params[:id]) + customer = Stripe::Customer.create( + email: params[:email], + source: params[:stripeToken], + description: 'slowfood client' ) - charge - end + +charge = Stripe::Charge.create( + customer: customer.id, + amount: order.order_total, + currency: 'sek' +) + charge +end ``` -We are calling the Stripe gem to create a customer but also to make charge the customer. -You will be getting errors now, we need to fix that by adding the stripe gem to our Gemfile. +We are calling the Stripe gem to create a customer but also to make charge the customer. + +You will be getting errors now, we need to fix that by adding the stripe gem to our Gemfile. `gem 'stripe-rails'` Run the tests again and your new error message should something in the lines of ```bash - Stripe::AuthenticationError: - No API key provided. Set your API key using "Stripe.api_key = ". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions. +Stripe::AuthenticationError: + No API key provided. Set your API key using "Stripe.api_key = ". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions. ``` This means that we need to use our API key that Stripe has provided for us. - - diff --git a/guides/react_checkout_guide/18_adding_credentials.md b/guides/react_checkout_guide/18_adding_credentials.md index 0954b7e8..b7cf610d 100644 --- a/guides/react_checkout_guide/18_adding_credentials.md +++ b/guides/react_checkout_guide/18_adding_credentials.md @@ -52,7 +52,7 @@ Add the following gem to the Gemfile `gem 'stripe-ruby-mock', '~> 2.5.6', require: 'stripe_mock'` -Then we want to update the rails_helper to add the mock functionality +Then we want to update the `rails_helper` to add the mock functionality Add `require 'stripe_mock'` to the rails_helper and add the configuration to RSpec diff --git a/guides/react_checkout_guide/19_finalizing_the_payment.md b/guides/react_checkout_guide/19_finalizing_the_payment.md index 49e7c29e..f143148c 100644 --- a/guides/react_checkout_guide/19_finalizing_the_payment.md +++ b/guides/react_checkout_guide/19_finalizing_the_payment.md @@ -3,32 +3,32 @@ Next up we want to modify the test with the mocked data. ```rb - before do - put "/api/orders/#{order.id}", - params: { activity: "finalize", - stripeToken: StripeMock.create_test_helper.generate_card_token, - email:'user@mail.com'} - end +before do + put "/api/orders/#{order.id}", + params: { activity: "finalize", + stripeToken: StripeMock.create_test_helper.generate_card_token, + email:'user@mail.com'} +end ``` And finally update the perform_stripe_payment method in our controller. ```rb - def perform_stripe_payment - order = Order.find(params[:id]) - customer = Stripe::Customer.create( - email: params[:email], - source: params[:stripeToken], - description: 'slowfood client' - ) - - charge = Stripe::Charge.create( - customer: customer.id, - amount: order.order_total.to_i * 100, - currency: 'sek' +def perform_stripe_payment + order = Order.find(params[:id]) + customer = Stripe::Customer.create( + email: params[:email], + source: params[:stripeToken], + description: 'slowfood client' ) - charge.paid - end + +charge = Stripe::Charge.create( + customer: customer.id, + amount: order.order_total.to_i * 100, + currency: 'sek' +) + charge.paid +end ``` Run your tests now and they should be going green. So Stripe added.