تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Recap New Components0:00

Welcome back! Between this lesson and the last one, we had a time skip, just like the ones you have in movies and TV shows. It's not a lot, though. Let's see what changed. First, I created a controller called CheckoutController inside the Order module, and I also created a CheckoutRequest. Inside the Payment module, I created a fake payment provider called PayBuddy, similar to... well, you know which one I'm talking about. The idea of PayBuddy is for us to use it as a fake payment provider during our tasks, just so we don't have to set up Stripe or something like that. And thirdly, I created a task inside Tasks, HTTPControllers,

Walking Through Checkout Test0:32

just so we don't have to set up Stripe or something like that. And thirdly, I created a task inside Tasks, HTTPControllers, CheckoutControllerTask. Let's see what we have here. It's not very complex. And this is what we're going to focus on in this lesson. We are going to build a very simple, a very naive implementation of a checkout. Let's go line by line. First, we are creating a User using Factories. Then, we are creating a couple Products. Two Products, to be exact. And I'm using Eloquent Factory's Sequence class to pass some attributes to them. The first one is a very expensive air fryer, which costs $100.

Sequence class to pass some attributes to them. The first one is a very expensive AirFryer, which costs $100. And it has 10 units in stock. And the second item is a MacBookPro M3. So we are in the future here. Remember, we had a time skip. Which costs $500. And it's super cheap, probably because this is 2035. Then, we have a paymentToken. Just like you would have with Stripe or with PayBuddy's cousin, PayPal. And we're just calling the validToken method on PayBuddy to get a valid token. Then, we are executing a request. We're acting as the User we created. We're sending the post

to get a valid token. Then, we are executing a request. We're acting as the User we created. We're sending the post request to the orderCheckout route. And we're passing the paymentToken and the products we want to purchase. Then, we assert we got a 201. We fetch the last Order. We assert that the user of the last Order, the owner, is the User we created. And then, we do some assertions on the price of the Order, the status, the paymentGateway that it was used. And then, on the orderLines. So, we're going to go through each one of those individually. But the point is to actually test the implementation so we can get something going. Now, before you go crazy about this implementation,

Reviewing PayBuddy Provider2:08

each one of those individually. But the point is to actually test the implementation so we can get something going. Now, before you go crazy about this implementation, don't worry about it. We're going to make it better. This is just an initial run. It's meant to not be perfect. We're going to learn as we go and also change things as we go. Alright, let's go to the top of the SaaS. Actually, let's take a look at the PayBuddy class real quick. As you can see, really simple. We have a charge method, which returns some fake data. We have a make method instantiated, a validToken method, and an invalidToken method, which we're going to use to test our checkout functionality. Alright, let's go back to the SaaS. I'm going to run

and an invalid token method, which we're going to use to test our checkout functionality. Alright, let's go back to the SaaS. I'm going to run this. Obviously, it fails because we're not getting the 201 and because we don't have anything on this controller. So, let's think about the most simple way we could implement this. If we go to the CheckoutRequest class, you're going to see we don't have a lot. We just have some rules. We must receive a paymentToken. We must receive some products. If we go to the routes file, you can also see that we just have one route and it requires authentication. Okay, let's go back to this. Alright, so we know what we must do. We must

Implementing Checkout Controller3:12

you can also see that we just have one route and it requires authentication. Okay, let's go back to this. Alright, so we know what we must do. We must create an order. So, we must use the Order model to create a record and then we must create order lines for each one of the products that were meant to be purchased. Let's go back to our CheckoutController and let's think how we could implement this. Well, the first thing we have to do is to issue a payment with PayBuddy, right? I'm going to instantiate PayBuddy saying PayBuddy::make() and then I'm going to create a variable called charge, which is going to be PayBuddy::charge(). We have the payment token on the $request, so we can use the input() method to save payment token.

a variable called charge, which is going to be PayBuddy charge. We have the payment token on the request, so we can use the input method to save payment token. The amount in cents. So, let's create a variable called orderTotal. How do we calculate the orderTotal? Well, I'm not sure, so I'm going to comment this for now and fetch the products first. We have that in an array so we can say products. Since we need both the product and the quantity for now, let's map that array. So, array_map, pass a closure here and pass the array of products, which is products, right? If we do a dump

the array of products, which is products, right? If we do a dump and die on this productDetails array, let's see what we get. I'm going to rerun the task. It's failing. Unexpected token at line 19. Okay, let's comment this for now. So, you can see that we have an ID and we have a quantity. Let's use that to map this into something more meaningful. We're going to return an array called the product and we're going to find the product in our database. So, we have the productID on this productDetails array, right? Let's do productDetails.ID

in our database. So, we have the product_id on this product_details array, right? Let's do product_details_id and then we also need the quantity, right? Because we need to use that not only to fill the order_line, but also to calculate how much we should charge this user. Okay, cool. So, let's dump what we have on this products array now. Let me rerun the tasks. Okay, so we have the product and then we also have the quantity. Cool, perfect. Now we have the data we need to calculate the order_total. To make our lives easier, let's turn this product into a collection. You might be familiar with Laravel collections. They are lovely. So,

To make our lives easier, let's turn this product into a Collection. You might be familiar with Laravel collections. They are lovely. So, we want to map the products we have. Let's call the map method and now we're just going to paste what we have on the top. This is just a much better API. Let's get rid of this. Now, to find out how much we have to charge the User, we can do a very simple calculation. We can fetch the products we have and we can easily reduce here, but we can also use the sum method. So, we can use the sum method. We're going to pass a closure to it, which is each element in the collection.

but we can also use the sum method. So, we can use the sum method. We're going to pass a closure to it, which is each element in the collection. Let's call it productDetails and what we want is the quantity multiplied by the productPrice. So, we can say productDetails product and we have the price in cents. I'm going to add this to a new line just to make it easier to read and if we dump orderTotal now, let's see what we have. Okay, we have $60,000, which is

to make it easier to read and if we dump orderTotal now, let's see what we have. Okay, we have $60,000, which is really $600 and that's correct. So, we have the total. Alright, so we're instantiating FadeBuddy. We're passing the paymentToken. Now, we must pass the orderTotal, which we have over there and we also have to pay by something. I think it is the paymentDescription. What's going to show up on the statement? Okay, it's going to be lower cast. Actually, let's go with modularization. Okay, that's better. Alright, so if we dump this charge, let's see what we get. We get an array, which is the reason I'm returning an array

Okay, that's better. Alright, so if we dump this charge, let's see what we get. We get an array, which is the reason I'm returning an array here is because I want to emulate what other payment providers do. Most of them return an array. Some of them have proper SDKs and actually return an object. That doesn't happen very often, so that's why I chose to return an array. Okay, we already have the charge, which is the most important thing or one of the most important things. What do we need now? We need an order first, so let's instantiate whatever we need. We need an order and we're going to need orderLines. However, we need the order first. We need to have the order to then create the orderLines. So again, remember the most simple implementation.

However, we need the order first. We need to have the order to then create the order lines. So again, remember the most simple implementation we can refactor later. Let's see what do we have to fill in the order. We have to fill the paymentId, we have to fill the status, the paymentGateway, the total in cents, and also the paymentIT. There we go. So let's start with the paymentId, which we already have. It's on the charge and it's the IT property. We need to fill the status. Well, since it either goes through or it fails, this must be paid. We have the paymentGateway. Let's just go with PayBuddy for now.

it either goes through or it fails, this must be paid. We have the payment gateway. Let's just go with PayBuddy for now. And then we have the total in cents, which we also have. So orderTotal and actually let's rename that to something more meaningful. orderTotalInCents, like this. Alright, let's go back to this. I think that's pretty much all we need. Oh, we also need the userID, right? So we can say that the userID, we have the request object, we can call user, and then we can get its ID. If we call order.save,

we have the request object, we can call user, and then we can get its ID. If we call order->save(), let's see what happens. It's failing. We got it to 100. We expected it to 1. Let's go ahead and dd() this. There we go. We have an order. We see its payment ID, we see the status, payment gateway, everything we need. Alright, let's refactor this. Since we already know everything we need, we can use the order::create() method. And I always like to call the query() method first. And then we can say that we have the payment ID, which is chargeId. We have the status, which is paid.

And then we can say that we have the paymentId, which is chargeId. We have the status, which is paid. We have the paymentGateway, which is PayBuddy. And we have the totalInCents, which is orderTotalInCents. We have the userId, which we can grab from the request object. And that's pretty much it. So now we can get rid of this and still have it. For now, let's return our response, which for now is just 801, right? We're not really passing anything meaningful. I'm actually going to return some JSON here and empty JSON and then 201. And let's run our test.

Adding Model Relationships9:36

anything meaningful. I'm actually going to return some JSON here and empty JSON and then 201. And let's run our test. Let's see where it's failing. So call to memory function is on null. If we go to line 45, which is this one. So we're calling the user method on Order. And again, I meant for this to be task driven. Let's see where the stakes is. And we're calling this on null, which means that order user is returning null. I know why. It's because we haven't set up any relationships. So we can start by creating a user relationship. So this belongsTo a User. Let's add our return type here.

start by creating a User relationship. So this belongs to a User. Let's add our return type here. Boom. Let's rerun the task. Okay. Now it's failing at 52. 52 is orderLine. So if we do a dump and die on orderLines and rerun this, we're getting no. Also because we haven't set the relationship. Just like we have orderLines, we can create a lines relationship here. This time it has many orderLines. And now we have both relationships we need. Now we get zero instead of

it has many OrderLines. And now we have both relationships we need. Now we get zero instead of no, but we still don't have any OrderLines. And that's because we're not creating them. Now that we have the Order, it's fairly easy to create the OrderLines. We have the Order, we have the products, the array we created, and now it's pretty easy. Easy peasy. So we can say that for each product we created, like this, we want to create one OrderLine. And we can use the relationship method here. So we can say orderLines, we're accessing the relationship here in this method on the Order object. We want

relationship method here. So we can say orderLines, we're accessing the relationship here in this method on the order object. We want to create a record. First we want to pass the productId and I'm actually on product. So we have product, product, productQuantity. So I want to access the product that we have here and I want to fetch its ID. We can also pass the price and cents. And that's pretty much all we need I think. Let's run this. Still failing. Got a 500. Product, price, and cents. Okay. So it's product, price, and cents. And then how many products we bought.

Product, price, and cents. Okay. So it's product, price, and cents. And then how many products we bought. Like this. Let's run our task now. Still failing. Let's see what went wrong. OrderLines, product, price, and cents. Huh. Okay, found the culprit. We didn't update this. And the reason I like to leave this in the recordings is if you ever stumble on this, this happens to everyone, including myself. We were just missing this on the fillory. All right, let's rerun this. One of the tasks are passing. One of them is failing. The one that's passing is this one.

Handling Invalid Payment Tokens12:16

Fillory. All right, let's rerun this. One of the tasks are passing. One of them is failing. The one that's passing is this one. So we got what we wanted. We're creating the Order. We're creating the OrderLine. We're charging the User. The task that's failing is when we pass an invalid token. So we have a User, we have a Product, we have an invalid token. We want to make sure something fails. Let's fix for this task now. If we look at PayBuddy, you can see that when we have an invalid token, it throws an exception right here in the validateToken method. So let's execute this task by itself.

it throws an exception right here in the validateToken method. So let's execute this task by itself using the filter method. And I should have done this from the beginning. I should only run this task first. And the reason I didn't is I'm using the task notations, which is a feature from phpunit, and phpStorm does not yet support it. So it runs all the tasks on the file. As you can see, it's failing. We are throwing an exception. And if we scroll up, yes, the given payment token is not valid. So all we need to do is we just need to handle this exception like this. Let's catch this very generic RuntimeException.

So all we need to do is we just need to handle this exception like this. Let's catch this very generic RuntimeException. And if that happens, we want to throw a ValidationException. This is from Laravel. We can pass a message. And we can say that the paymentToken, obviously, we do not want to say that to the user. So we can say something like, we could not complete your payment. Let's see what happens now. Okay, that's an interesting one. I'm also going to keep this in the video. We're not getting a 42, but we're getting a 302 because we're not passing the JSON request. So what I was trying to redirect is to another page.

but we're getting a 302 because we're not passing the JSON request. So what I was trying to redirect is to another page so we can show the validation errors. Let's replace this with post JSON. So we're sending a JSON request now. And let's replace assertInvalid with assertJsonValidationErrors. And we can also specify which fields we want to have errored. In our case, we're going to say paymentToken. If we rerun both tasks, they're both passing. And if we focus only on this one, I'm going to copy this and manually pass it using the filter option. You can also see it's passing. We have

one, I'm going to copy this and manually pass it using the filter option. You can also see it's passing. We have 11 assertions. So we are asserting everything about the order and everything about the order lines as well. You can see that I'm using foreach here. It was just to make my life easier and this video shorter. But we're basically going through each one of the products we created and asserting that first we can see them on the order line. So we're finding the first one that matches the given product ID. And then we are asserting that it has the correct price and the correct quantity because we only wanted to order one item. Now, obviously,

we are asserting that it has the correct price and the correct quantity because we only wanted to order one item. Now, obviously, in real life, you would want to have multiple tasks. You want to have tasks with multiple items, tasks with one item, tasks with multiple items and multiple quantities. And we're going to get to there using data providers. Not in this video, though. So we've implemented that. And now you might be thinking, okay, what's the purpose of this lesson on this course? This is not an e-commerce course. And the reason is, if we go back to the controller, let's see how many different modules we touched. So we are on the order module. We also talked to the payment module,

controller, let's see how many different modules we touched. So we are on the Order module. We also talked to the Payment module, which is where PayBuddy is. We have also spoke to the Product module, which is where the product is. So we have three modules in play here: Order, Payment, and Product. That means that we have cross-boundary communication. And this is where things get interesting. How can we write code in such a way that each module is as isolated as possible, can still communicate to other modules and are still maintainable? And that's where we're going to focus now. Obviously, this lesson was just to get things started and show you guys

and are still maintainable? And that's where we're going to focus now. Obviously, this lesson was just to get things started and show you guys how I'm thinking about building this checkout flow. But in the next lessons, we're going to see how to extract some of this functionality to other modules, how to correctly communicate to them. And also, something you might have noticed is we're dealing with a lot of arrays here. First, there's no autocompletion. You might have noticed that when I was writing those queries, I didn't know which fields I had access to, which is a common problem. There's no type safety as well. And it's just overall very hard to understand. So another thing we're going to talk about is

problem. There's no type safety as well. And it's just overall very hard to understand. So another thing we're going to talk about is how to encapsulate this kind of data into objects that represent intent, that represent the things we need, and also give us some type safety and some developer experience with autocompletion. Alright, I think this lesson is long enough. Before we wrap this up, let's just go through this flow very quickly. First, we're mapping something we receive on the post request, which are the products the user wants to purchase. And we're returning something that includes both the product model as well as the quantity.

on the post request, which are the products the user wants to purchase. And we're returning something that includes both the product model as well as the quantity so that we can calculate how much they owe and how much they should be charged. Then we're calculating the order total in cents. We're just summing this array and then multiplying the quantity by the product's price in cents. Then we instantiate PayBuddy, our favorite payment provider. We charge the user. We create an order with all the information we've gotten so far, which is the payment ID that PayBuddy returned to us, the order status, the payment gateway, which is PayBuddy, the order total in cents, and the user ID.

Decrementing Product Stock17:36

which is the paymentId that PayBuddy returned to us, the order status, the payment gateway, which is PayBuddy, the order total in cents, and the userId. And then we create the order lines for this order. Now, one thing that is missing, though, and I just noticed now, is we're not decreasing the stock. So this product would run out of stock. Let's write a test for that real quick. Let's scroll down a little bit, and we're going to add this to the end of the test. We want to assert that each one of those products, and they had 10 units in stock right here, we purchase 1 of each, which means they should now have 9 units in stock for each one of them. All right, the first thing we've

we purchase one of each, which means they should now have nine units in stock for each one of them. All right, the first thing we've got to do is we've got to refresh those products. Remember, between creating them and doing these assertions, we had an HTTP call, and things happened. The database was changed, so we need to refresh it. Thankfully, Laravel has a method on the collection called refresh. So we can just call product->refresh(), and it's going to refresh the collection. And now we can add two assertions. The first one is we want the first product to have a stock of nine, and we just want to copy this and apply it.

add two assertions. The first one is we want the first product to have a stock of nine, and we just want to copy this and apply it to the second item, which is the last item as well. If we run this, it fails. I'm sorry, it's not refresh, it's fresh. My apologies. So product's fresh, and then we can run our assertions. What's this? It just refreshes the model, so it basically refetches them from the database. Let's run this. Fail asserting the ten matches expected nine. That makes sense, because we're not decreasing the stock. Let's go to

Let's run this. Fail asserting the ten matches expected nine. That makes sense, because we're not decreasing the stock. Let's go to our code, and right here where we are going through each one of the Product models and creating a line item, we can also decrement the stock. We can say $product and access the product item, and say decrementStock. That is a model method, and it will decrease by one, or whatever we specify as the second argument, a quantity in the database in the column. We're specifying the stock column. Let's rerun our code, and now we're passing. Now you might have noticed no autocompletion whatsoever.

We're specifying the stock column. Let's rerun our code, and now we're passing. Now you might have noticed no autocompletion whatsoever. I do not know what product is, neither does phpStorm. We're going to talk about that in the next lesson. Don't worry about it. Alright, our tests are back to green, and our checkout flow is working. It doesn't look really good, but it's working. We are going to make it better in the future. See you in the next lesson. Bye-bye.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟