- Create test database
- Usually you just create a sqlite database is a lot faster
- Yet is great to have a real database instead
- in phpunit.xml and add
<server name="DB_DATABASE" value="cart_testing"/>
- Now when we run tests we will use cart_testing database
- code ~/.zshrc
alias phpunit="./vendor/bin/phpunit"
use Tests\TestCase;
instead ofPHPUnit\Framework\TestCase
- For each test that we have 10 . Tests\Feature;
- We have
use Illuminate\Foundation\Testing\RefreshDatabase;
- Add it to the main TestCase.php
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, RefreshDatabase;
}
- Delete Feature/ExampleTest.php
- use phpunit to get green
- Delete Unit/ExampleTest.php
- After we write a feature we write test for it.
- php artisan make:model -m
- Create a route in routes/api
- check
http://cart-api.test/api
in Postman Preview php artisan make:migration add_order_to_categories_table --table=categories
php artisan make:test Models\\Categories\\CategoryTest --unit
php artisan make:factory CategoryFactory
- composer require laravel/helpers
- Call to a member function connection()
https://stackoverflow.com/questions/42512676/laravel-5-unit-test-call-to-a-member-function-connection-on-null
check if the test class is extending use Tests\TestCase; rather then use PHPUnit\Framework\TestCase;
Laravel ships with the later, but Tests\TestCase class take care of setting up the application,
otherwise models wont be able to communicate with the database if they are extending PHPunit\Framework\TestCase.
- laravel/framework#34209
- Create traits to reuse code in Categories for other Models
- php artisan make:controller Categories\CategoryController
- create route in api.php
- Route::resource('categories', 'Categories\CategoryController');
- Go to Postman and check the api
- http://cart-api.test/api/categories
- https://dev.to/seankerwin/laravel-8-rest-api-with-resource-controllers-5bok
- In APi you only need to create 1 route and laravel 8 will handle the rest
- Route::resource('categories', CategoryController::class);
- create index() in CategoryController and check the route in Postman
- Now we going to use Category Resources.
- php artisan make:resource CategoryResource
- php artisan make:test Categories\CategoryIndexTest
- phpunit tests/Feature/Categories/
- php artisan make:model Product -m
- php artisan make:test Product\ProductTest --unit
- php artisan make:controller Products\ProductController
- php artisan make:resource ProductIndexResource
- php artisan make:migration add_description_to_products_table --table=products
- php artisan make:test Product\ProductIndexTest
- php artisan make:factory ProductFactory
- ProductResource is a standard resource that extends ProductIndexResource
- php artisan make:resource ProductResource
- extend class ProductResource extends ProductIndexResource instead of JsonResource
- Then merge with array_merge array_merge(parent::toArray($request)
- php artisan make:test ProductShowTest
- Product = things we Testing | Show = Action we are testing | Test
- Now we can show list of products and show a product
- php artisan make:migration create_category_product_table --create=category_product
- creates belong to many relationship
- php artisan make:test Products\ProductScopingTest
Reason we use CORS is that our API and front end can be in different domains so we need to tell the api that request from the front end using a certain domain we specify is allowed
- https://github.com/fruitcake/laravel-cors
- composer require fruitcake/laravel-cors
- in app/Http/Kernel.php
protected $middleware = [
\Fruitcake\Cors\HandleCors::class,
// ...
];
- php artisan vendor:publish --tag="cors"
- Now we can add our origins in config/cors.php
- 'allowed_origins' => ['*'], means anything can access it
- 'allowed_origins' => ['yoursite.com'], only your site can access it.
- Remember to add 'paths' => ['api/*'], to allow cors path
- example with shoes
nike air max
colors
blue
black
white
sizes
uk 9
uk 10
- when he add product to cart what we actually adding is product variation
- php artisan make:model ProductVariation -m
- php artisan make:resource ProductVariationResource
- php artisan make:factory ProductVariationFactory
- php artisan make:model ProductVariationType -m
- php artisan make:migration add_product_variation_type_id_to_product_variations_table --table=product_variations
- Adding Product variation
When we use product variation result we saying this->id but that doesn't work since we are trying
to look for a key on a collection, is not expecting something to be grouped by something else
What we can do is check, $this->resource and check if it an instance of Collection
We want to return ProductVariationResource, but we want to return collection for each.
What is happening here is that if we are grouping items each of them groups will be an individual collection
with items inside of themselves
What Resource trying to do without this if statement is trying to access $this->id which will not work
What the if statement does is goes into each of the keys keys that we group by and then go ahead and
return a collection of products variation
- php artisan make:test Product\ProductVariationTest --unit
- php artisan make:factory ProductVariationTypeFactory
- Verify Tests\Feature\Product\ProductIndexTest::test_it_shows_a_collection_of_products
- Notice nothing is wrong with test.
- So we go to postman and check http://cart-api.test/api/products
- and we see data [] empty even through we do have products
- reason for this is the Scoper
- Go to CategoryScope and do a dd('abc')
- Then send in postman and we see the output 'abc'
- But our CategoryScope should not be called and run and filter when we calling http://cart-api.test/api/products
- since the $value going to be null and cause error in test
- We going to fix issue inside Scope.php we want to skip filtering by things that don't exist within the query string
- Create function in Scoper.php called limitScopes
- return a limited collection of scopes
- Base on what is on the query string
- Then add the protected function foreach ($this->limitScopes($scopes) as $key => $scope) {
- Now we limiting the scopes and not running the apply filtering if we don't need to on the test.
- move Model Product.php methods to a trait
- So you can reuse it for other Models
- https://github.com/moneyphp/money
- composer require moneyphp/money
- On front end when it says get new cart total
- We not going to do any calculation on the client side
- We going to do all the adding up different prices, adding on shipping
- All that kind of stuff in API itself
- All we need to output in the API is the formatted price
- public function formattedPrice() will not work
- So you need to add public function getFormattedPriceAttribute() so we pull out dynamically
- in HasPrice trait
- When we try to Access Price attribute
- It will automatically give us Custom Money class
- Product variation can have different price
- Key here is that if the product variation doesn't have a price
- It needs to inherits it from the base point product the default price in products table
- if price doesn't exist from variation we need to inherit it from the parent product
- We need to overwrite price attribute from HasPrice.php
public function getPriceAttribute($value)
{
// original value we have in database
// When we try to Access Price attribute
// It will automatically give us Custom Money class
return new Money($value);
}
- We overwrite since we want to target to specifit product related
public function getPriceAttribute($value)
{
if ($value === null) {
// will return Money instance since you are using Attribute
return $this->product->price;
}
return new Money($value);
}
- when price varies is true it means the price is different from default cost.
- So we add new price_varies attribute to the product data result
- http://cart-api.test/api/products/coffee
- If product variation doesn't have a price it inherits price from the parent
- Check if price varies
- We are not attaching stock to products, we are to products_variations
- We will build a stock ordering table will check which product have been ordered
- Will give you dynamic value for the stock that you have
- php artisan make:model Stock -m
- php artisan make:factory StockFactory
- Order table will be able to create orders for particular user link this in to proper variation in order.
- From that we can deduce how much stock we actually have based on the quantities that have been ordered
- php artisan make:model Order -m
- Create pivot table that tells us how much been ordered
- php artisan make:migration create_product_variation_order_table --create=product_variation_order
- How orders work
- We take cart content of the user cart place them into product_variation_order
- Then create order when it been successful
- Use ""SQL Query"" to create a view
create view products_view as
select * from products
- cart/Views/products_view in database
- when we create a new product in product table, the products_view will give us the updated version.
- delete products_view since this was an example
- The view we going to create will be a list of all the products variations and what the current stock is
- with the sum of the stocks(table) subtracted from the order quantity(product_variation_order) that have been made
- As well boolean flag to check if this is out of stock or in stock.
- Just so we have it in the table, we don't need to represent this in code
- We want to drop VIEW if exist since we want to gradually
- We are selecting all the products_variations
drop view if exists product_variation_stock_view;
create view product_variation_stock_view as
select product_variations.product_id as product_id
from product_variations
- We get all the products id from the product_variations table
drop view if exists product_variation_stock_view;
create view product_variation_stock_view as
select
product_variations.product_id as product_id,
product_variations.id as product_variation_id
from product_variations
- Now we will use join
- Will use (id) as the primary
drop view if exists product_variation_stock_view;
create view product_variation_stock_view as
select
product_variations.product_id as product_id,
product_variations.id as product_variation_id
from product_variations
left join (
select stocks.product_variation_id as id
from stocks
group by stocks.product_variation_id
) as stocks using (id)
- sum(stocks.quantity) as stock will give us the total for this particular variation
- then group it by the variation id
DROP VIEW IF EXISTS product_variation_stock_view;
CREATE VIEW product_variation_stock_view AS
SELECT
product_variations.product_id AS product_id,
product_variations.id AS product_variation_id,
SUM(stocks.quantity) AS stock
FROM product_variations
LEFT JOIN (
SELECT stocks.product_variation_id AS id,
SUM(stocks.quantity) AS quantity
FROM stocks
GROUP BY stocks.product_variation_id
) AS stocks USING (id)
group by product_variations.id
- Now we can test it by adding another 100 in stock table
- Now we give refresh to product_variation_stock_view and see 200
- if stock none existing or we don't have any stock
- We want this to represent 0
- For example create another stocks table item and have 0 quantity
- We will see is 0 in product_variation_stock_view
- But if we don't add stock 0 we going to see null value
- To resolve this we going to use COALESCE to return 0 as default
coalesce(SUM(stocks.quantity), 0) AS stock
- Now we join the product_variations_order table since it has the quantity we can substract from total amount of stock
- We will use JOIN for this
left join (
select
product_variation_order.product_variation_id as id,
SUM(product_variation_order.quantity) as quantity
from product_variation_order
group by product_variation_order.product_variation_id
) as product_variation_order using (id)
group by product_variations.id
- Substract the total when another user makes a purchase
coalesce(SUM(stocks.quantity) - SUM(product_variation_order.quantity), 0) AS stock
- Now we can test it by adding more stock in stocks table
- Then again by adding in product_variation_order so that it substract from the total stock in product_variation_stock_view
- Now we going to add default 0 if (product_variation_order doesn't exist
coalesce(SUM(stocks.quantity) - coalesce(SUM(product_variation_order.quantity), 0), 0) AS stock
- Now try doing same by adding a new stock item
- We will do it in SQL editor by using case value and check if greater than > 0 then we represent it as true value
- else represent a false value
- Now we can see the in_stock column in product_variation_stock_view
coalesce(SUM(stocks.quantity) - coalesce(SUM(product_variation_order.quantity), 0), 0) AS stock,
case when COALESCE(SUM(stocks.quantity) - coalesce (sum(product_variation_order.quantity), 0), 0) > 0
then true
else false
end in_stock
- Now we test it by making an order in product_variation_order for 50 which will turn in_stock to false
- What we can do now is create a migration for this
- We will convert this view into a migration.
DROP VIEW IF EXISTS product_variation_stock_view;
CREATE VIEW product_variation_stock_view AS
SELECT
product_variations.product_id AS product_id,
product_variations.id AS product_variation_id,
coalesce(SUM(stocks.quantity) - coalesce(SUM(product_variation_order.quantity), 0), 0) AS stock,
case when COALESCE(SUM(stocks.quantity) - coalesce (sum(product_variation_order.quantity), 0), 0) > 0
then true
else false
end in_stock
FROM product_variations
LEFT JOIN (
SELECT stocks.product_variation_id AS id,
SUM(stocks.quantity) AS quantity
FROM stocks
GROUP BY stocks.product_variation_id
) AS stocks USING (id)
left join (
select
product_variation_order.product_variation_id as id,
SUM(product_variation_order.quantity) as quantity
from product_variation_order
group by product_variation_order.product_variation_id
) as product_variation_order using (id)
group by product_variations.id
php artisan make:migration product_variation_stock_view
- We will add it using DB::statement()
DB::statement("
CREATE VIEW product_variation_stock_view AS
SELECT
product_variations.product_id AS product_id,
product_variations.id AS product_variation_id,
coalesce(SUM(stocks.quantity) - coalesce(SUM(product_variation_order.quantity), 0), 0) AS stock,
case when COALESCE(SUM(stocks.quantity) - coalesce (sum(product_variation_order.quantity), 0), 0) > 0
then true
else false
end in_stock
FROM product_variations
LEFT JOIN (
SELECT stocks.product_variation_id AS id,
SUM(stocks.quantity) AS quantity
FROM stocks
GROUP BY stocks.product_variation_id
) AS stocks USING (id)
left join (
select
product_variation_order.product_variation_id as id,
SUM(product_variation_order.quantity) as quantity
from product_variation_order
group by product_variation_order.product_variation_id
) as product_variation_order using (id)
group by product_variations.id
");
- Now we create the down() to drop the table
public function down()
{
DB::statement("DROP VIEW IF EXIST product_variation_stock_view");
}
- Now we delete the product_variation_stock_view since it should not be there
- php artisan migrate
- Now where ever our project go we have up to date dynamic stock information that tell us if particular product is in stock
- Create stock() method in ProductVariation
- what we want to get back from this relationship is a product variation instance
- we not interested in the product variation what we are interested is the pivot information the stock
- Reason we use belongsToMany is that we can access that pivot information
- sum up each of the product variations in Product.php called stockCount()
- Add some profiling to see within our JSON response to see how many queries we are running
- We more than likely end up with problems with the relationships
- To do this we will install https://github.com/barryvdh/laravel-debugbar
composer require barryvdh/laravel-debugbar --dev
- We will add some middleware which will add on this debug information to our json end point
- php artisan make:middleware ProfileJsonResponse
- We can use this to output any information inside Json response
- in ProfileJsonResponse fill the handle test it works with dd('works') in Postman
- Then fill the handle properly with $request->has('_debug')
- Check with http://cart-api.test/api/products/coffee?_debug
- In postman "nb_statements": 20, is how many statements been run 12.In here we can check for SQL statement that running to look for any duplication
- To check if you have an n + 1 problem
- Duplicate a stock in stocks table
- Duplicate row in product_variation
- Then check in postman to see "nb_statements": 23, incremented
- Problem is we should not have extra queries as we add extra records
- In ProductController show method, added
$product->load(['variations', 'variations.type']);
- To reduce "nb_statements" by using load
- This will reduce "nb_statements": 17 since we don't have to iterate over each one.
- The other thing we need to take into account is th stock
- $product->load(['variations', 'variations.type', 'variations.stock']);
- Remember we have relationship set up for stock so we need to pull that in as well.
- It will reduce "nb_statements": 11 again
- We can take out variations since that already been accessed.
- Go to Product.php model to see what we are pulling in
- Then go to ProductVariation.php and see if there anything here
- Then scroll down in postman and see each of the queries that were made
- We notice we have multiple request to our products table
- "sql": "select * from "products" where "products"."id" = 1 limit 1",
- Lets add product as well $product->load(['variations.type', 'variations.stock', 'variations.product']);
- "nb_statements": 5 was reduced again
- Now lets check products page http://cart-api.test/api/products?_debug in postman
- We have "nb_statements": 11 queries we are executing
- We can do a search with postman of the queries we are executing "sql"
- we have product_variations stock so it looks like we should be loading that stock
- ProductController index() method '$products = Product::withScopes($this->scopes())->paginate(10);'
- We should see a reduce "nb_statements": 4
- You will notice with these changes it get a lot faster and respond time gets a lot quicker as well.
- https://github.com/tymondesigns/jwt-auth
- composer require tymon/jwt-auth
- php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
- php artisan jwt:secret
- will be used to decoding and encoding the payload
- config/jwt.php
- Secret placed on 'secret' => env('JWT_SECRET'),
- You can check it has been added JWT_SECRET in .env
- We will change The Time Live TTL 'ttl' => env('JWT_TTL', 60), to a higher value
- JWT_TTL=3000
- Go to config/auth.php and change 'guard' => 'web',
- to api 'guard' => 'api',
- Then change 'api' => [ 'driver' => 'token'
- Change it to jwt 'api' => [ 'driver' => 'jwt'
- Implement the JWTSubject to user model
- class User extends Authenticatable implements JWTSubject
- truncate user table since it doesn't have a hash password
- in dbeaver left click user table > tools > truncate > checkmark Cascade|Restart identity > start
- php artisan make:controller Auth\RegisterController
- http://cart-api.test/api/auth/register
- Laravel 7 had update where the
Route::post('register', 'Auth\RegisterController@action');
does not work anymore - To update it check this solution
https://stackoverflow.com/questions/57865517/laravel-6-0-php-artisan-routelist-returns-target-class-app-http-controllers-s
- We later going to create an observer inside the user model.
- So that everytime we create new user it automatically hashes the password
- Create user using postman click on Body > form-data > fill (email, password, name)
- Now we will crease a Resource for the endpoint we will create in future to gather all the information from a particular user
- When we build any application we can create Private and Public user resource
- PrivateUserResource will only return when is that actual user that requested that information
- PublicUserResource will be public for all users to see. For example a review for a product, if you implemented reviews
- You will a PublicUserResource that don't contain the user email and that other private information
- php artisan make:resource PrivateUserResource
- php artisan make:request Auth\RegisterRequest
- Has to be unique on users table under email unique:users,email
- In newer Laravel you get a 404 page instead of JsoUn error
- To fix it in Postman click on Headers and write Key: Accept Value: application/json
- Has to be unique on unique email from the users table unique:users,email
- This is suppose to be a feature test but we can get away with unit test for our user
- php artisan make:test Models\Users\UserTest --unit
- php artisan make:test Auth\RegistrationTest
- php artisan make:controller Auth\LoginController
- Reason we use action because is makes thing more tidy when you use a controller for a single thing
- 422 is validation error
- php artisan make:request Auth\LoginRequest
- php artisan make:test Auth\LoginTest
- http://cart-api.test/api/auth/me
- php artisan make:controller Auth\MeController
- http://cart-api.test/api/auth/login and get the token
- then use the token in http://cart-api.test/api/auth/me
- Click on Authorization tab, TYPE Bearer Token and paste token
- We don't want user to get access to MeController if they not authenticated
- So we use a __construct or middleware at the route
public function __construct()
{
$this->middleware(['auth:api']);
}
- This means that if we try to access http://cart-api.test/api/auth/me
- We get "message": "Unauthenticated."
- php artisan make:test Auth\MeTest
- Lets take a look at use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
- click on Concerns\MakesHttpRequests,
- then we find public function json($method, $uri, array $data = [], array $headers = [])
- As you can see is almost same signature we used only difference is we are also accepting that user here.
- All we need to do is recall the method and also pass the header with barren token for that particular user.
- php artisan make:migration create_cart_user_table --create=cart_user
- you have to have each item in alphabetic order cart_ and users
- Undefined table: 7 ERROR: relation "product_variation_user" does not exist
- to fix this error we can rename it in cart() method in User.php
return $this->belongsToMany(ProductVariation::class, 'cart_user')->withPivot('quantity');
- Another error
Not null violation: 7 ERROR: null value in column "quantity" violates not-null constraint
- To fix it php migrate:rollback then set a default of 1
$table->integer('quantity')->unsigned()->default(1);
- Goal for this part is an endpoint that allow us to specify within a json payload a list of products we want to add or product variations along side the quantities.z
- We going to allow for multiple products to be added at once which is very important
- In postman
http://cart-api.test/api/cart
- Click on Body and remove all columns we sending through like email, name and password
- Click on raw radio button and choose JSON(application/json)
- Then in raw write
- Lets say that for example on the client side you want to allow a guest to start adding items to their cart before they checkout
- When they checkout you want to create user account for them and then you want to add list of products to their cart
- Actually store them and persist them into the database
- Now if you were creating an end point like this
http://cart-api.test/api/cart
posting through a single product variation id in a single quantity - Lets say user added 20 item to that cart, what you will have to do is do 20 different http request to our api
- Add each of the items they have stored on the front end
- While if you allow multiple products to start with that means if you say on the client side storing them within a session in local storage for example
- Then you can make 1 single request for all of the products then is just 1 http request
- So is important to add multiple products to be added
{
"products": [
{ "id": 1, "quantity": 1 },
{ "id": 2, "quantity": 1 }
]
}
- php artisan make:controller Cart\CartController
- Login with Post man and get the token
- Remember to add token to Authentication Bearer Token
- Alternative you can add token to postman via an environment persist accross all the request.
- Next thing that is important is validate each of the products
- This is treacky since now we have json payload and we want to validate each of the products
- For example check if each of the id actually exist.
- php artisan make:request Cart\CartStoreRequest
- add ->withTimestamps to add date as well. in cart() method in User.php model
- php artisan make:test Cart\CartTest --unit
- php artisan make:test Cart\CartStoreTest
- you can use dd($response->getContent()); to see output from the failure in the terminals
- To see that the issue with test was that in AppServiceProvider we taking a Cart and returning a new user
- To fix it go to Cart Controller and mode Cart $car from controller into the store method instead
- Everything we add a product we want to increase quantity in the cart
- To do this we create method getCurrentQuantity() in Cart.php
- Duplicate in postman the tab for http://cart-api.test/api/cart
- then add the product number http://cart-api.test/api/cart/3
- Now in Body tab move from raw to x-www/form-uriencoded
- Then key > quantity and value > 5
- then see the routes with php artisan route:list and see the parameter is http://cart-api.test/api/cart/{cart}
- Then we want to rewrite the route parameters
Route::resource('cart', CartController::class, [
// paramenters we want to overwrite
'parameters' => [
'cart' => 'productVariation'
]
]);
- Then we check again the routes with php artisan route:list
- Now for the cart we accepting productVariation instead http://cart-api.test/api/cart/{productVariation
- In postman switch POST to Patch
- now you can update quantity with x-www/form-uriencoded
- php artisan make:request Cart\CartUpdateRequest
- php artisan make:test Cart\CartUpdateTest
- We doing testing in Step proccess 1) test_it_fails_if_unauthenticated 2) test_it_fails_if_product_cant_be_found()
- Had error since I mispelled required in CartUpdateRequest
'quantity' => 'requires|numeric|min:1'
- You can delete by creating a delete mething inside of Cart.php that will detach $this->user->cart()->detach($productId);
- php artisan make:test Cart\CartDestroyTest
- Create method empty() in Cart.php
$this->user->cart()->detach();
- Duplicate http://cart-api.test/api/cart/3 tab
- Then set it to GET request to http://cart-api.test/api/cart
- Need to Create index method for the endpoint to work
- Create resource for index() method to use
- php artisan make:resource Cart\CartResource
- Send POST to http://cart-api.test/api/cart make sure to log in and get token in Authorization
- then send a GET request to http://cart-api.test/api/cart to see the product
- We also need Base product it belongs to
- is not enough to say 250g since this is just a product variation
- What we can do is not reusing a ProductVariationResource since inside it we don't show the product
- php artisan make:resource Cart\CartProductVariationResource
- we can now extend the ProductVariationResource
- We can use array_merge to make it more flexible without having lots of things all over the place
- Now you can get access to Base Product and the other Variation Data in CartProductVariationResource
- Now since we have access to the Pivot as well we want show quantity
- We can change total to test it on the cart_user table
- Now using total we can get the total price of the quantity
$total = new Money($this->pivot->quantity * $this->price->amount());
- Now remember to use _debug to see how many query request we getting
http://cart-api.test/api/cart?_debug
- Now you can see the total queries to be "nb_statements": 11,
- Now test if we add more products using postman
- http://cart-api.test/api/cart Body raw
{
"products": [
{ "id": 3, "quantity": 1 }
]
}
- Change it to another product
{
"products": [
{ "id": 4, "quantity": 5 }
]
}
- Now you can see that it increased "nb_statements": 20,
- We can add in index() CartController a load to reduce nb_statements
- we try $request->user()->load('cart'); and then check Postman http://cart-api.test/api/cart?_debug
- To Realize that it doesn't work so we instead use $request->user()->load('cart.products');
- http://cart-api.test/api/cart?_debug
- "nb_statements": 12
- Since we are dealing with things like the stock $request->user()->load('cart.product.variations.stock');
- http://cart-api.test/api/cart?_debug and see it was reduced as well "nb_statements": 7
- Lets now add $request->user()->load('cart.product.variations.stock', 'cart.stock');
- now sql statement were reduced as well "nb_statements": 6
- You can check for more searching for "sql"
- php artisan make:test Cart\CartIndexTest
- An alternative is create API test in isolation and not much in feature test
- since feature test, is just seeing that we can access the endpoint that we can generally see that information
- You can also create a protected function in ProductVariationResource called getTotal to make code more organized
- Create method isEmpty inside Cart.php
- You could use
return $this->user->cart->count(); // 0
but you will have problems when item goes out of stock. - So is better to do a sum instead since it will be more accurate and avoid this issue.
- Sum up the items quantity in the cart and not actually the items that are in the cart
- Go to Postman and sign up
http://cart-api.test/api/auth/login
and get the token - then
http://cart-api.test/api/cart
and click authentication tab and paste the token in as Bearer token
- Send a Get request with postman to http://cart-api.test/api/cart
- You can now add items to cart_user then add products in stock.
- Make sure there are no orders in product_variation_order
- Now it give return the maximum amount of the current stock if your cart quantity is over that limit
- update pivot: it will change cart stock amount to the available stock amount when you add more items than currently in stock
- We also need to tell users if this change have happened to their cart
- Create protected $changed = false;
$this->changed = $quantity != $product->pivot->quantity
- Then create method called hasChanged() that gets the changed value
- This can be used if user tries to modify to purchase 200 items when that is not even an option
- Also useful when you want to stop people ordering more stock that is available
- See the type in Postman
http://cart-api.test/api/cart
- Lets check our endpoints with debugbar
http://cart-api.test/api/cart?_debug
"nb_statements": 9,- Lets reduce number by going to CartController.php
- Do a search for "sql" in Postman to see the common queries
- We can see that grabing stock count for product variation from the stock view we created is causing some problems
- It seems the problem is cased in Cart.php in sync() method
- We can add return in the start of method to see if is the one causing problems then count the amount of nb_statements
- and we see the number was reduced
- This is problem since we are calling the minStock method over in the productVariation
- As well minStock use the stockCount which require the information of the user as we use this
- we can fix this by doing the ego loading somewhere else perhaps service provider
- In AppServiceProvider register() we going to load what we need
- Now we check
http://cart-api.test/api/products/coffee?_debug
then do a search of "sql" - Seems it looks great so we test another link
http://cart-api.test/api/products?_debug
- This will help your page load faster when we reload page
- No matter the amount of items, we want it to load very quickly
php artisan make:model Country -m
- create a seeder to populate the table
- php artisan make:seeder CountriesTableSeeder
- there was error with seeder since you didn't set timestamps to fix it
- Go to the model Country.php and set $timestamps = false;
- php artisan make:model Address -m
- php artisan make:factory AddressFactory
- php artisan make:factory CountryFactory
- php artisan make:test Models\Addresses\AddressTest --unit
php artisan make:controller Addresses\\AddressesController
php artisan make:resource AddressResource
php artisan make:resource CountryResource
php artisan make:test Addresses\\AddressIndexTest
- Create store() Method in AddressController.php
- Send a Post request to
http://cart-api.test/api/addresses
- Then click on Body Tab to send data
php artisan make:request Addresses\\AddressStoreRequest
php artisan make:test Addresses\\AddressStoreTest
- We need to toggle default address when user has multiple addresses
php artisan make:migration add_default_to_addresses_table --table=addresses
- We going to add a default column to the address table
- Create static boot method on the Address.php model and do a dd($address)
- Then go to postman and send s POST to
http://cart-api.test/api/addresses
- We don't have default since we have not set it on fillable
protected $fillable = [
'name',
'address_1',
'city',
'postal_code',
'country_id',
'default'
];
- After sending POST we see is a string instead of Boolean so we create a setDefaultAttribute() method in Address method
- Create if statement that checks if user has a default address already and set all the newly created address default to false.
- Use Postman now and send 2 POST request to
http://cart-api.test/api/addresses
and first address should be default true and others false
php artisan make:controller Countries\\CountryController
- check
http://cart-api.test/api/countries
in postman to see all the countries
php artisan make:test Countries\\CountryIndexTest
php artisan make:model ShippingMethod -m
php artisan make:test Models\\ShippingMethods\\ShippingMethodTest --unit
php artisan make:factory ShippingMethodFactory
php artisan make:migration create_country_shipping_method_table --create=country_shipping_method
php artisan make:test Models\\Countries\\CountryTest --unit
php artisan make:controller Addresses\\AddressesShippingController
- Create route that requires user to be logged in, in api.php
- use postman and send a GET request to
http://cart-api.test/api/addresses/4/shipping
- Make sure you get a valid address id in address table
- You need to add the auth in the Controller, since it didn't work on the api route
public function __construct() { $this->middleware(['auth:api']); }
6. `php artisan make:resource ShippingMethodResource`
7. Only see shipping methods available for our own addresses
8. To resolve this make a policy
9. `php artisan make:policy AddressPolicy`
10. Then connect it on AuthServiceProvider.php
```php
protected $policies = [
'App\Models\Address' => 'App\Policies\AddressPolicy',
];
- Now add the policy to the method
public function action(Address $address)
{
// only see shipping methods available for our own addresses
$this->authorize('show', $address);
return ShippingMethodResource::collection(
$address->country->shippingMethods
);
}
- In the policy check if id match the currently logged user id
public function show(User $user, Address $address)
{
return $user->id == $address->user_id;
}
- Now send with postman GET request
http://cart-api.test/api/addresses/4/shipping
- You can use this with the checkout where we select an address and then recheck which shipping methods are available for that particular address.
php artisan make:test Addresses\\AddressShippingTest
$address = Address::factory()->create([
'user_id' => $user-id,
// we going to check that when we add shipping method to this country, that we are adding for this user address
// wrap the country definition so we have access to that entire country not just the id
'country' => ($country = Country::factory()->create())->id
]);
- You can also remove assertion to check if everything working correctly
- Unable to find JSON fragment: [{"id":1}] means you need to add Id to the ShippingMethodResource
- use postman and send a GET method to
http://cart-api.test/api/cart?shipping_method_id=1
- You also later add validation so that it doesn't accept none existing shipping_method_id
- Create test for money class
- php artisan make:test Money\MoneyTest --unit
php artisan make:migration add_address_and_shipping_to_order_table --table=orders
- Make test to check relationships between models
php artisan make:test Models\\Orders\\OrderTest --unit
php artisan make:factory OrderFactory
- We going to use const in the model which is an alternative to using ENUMS in the database, since they can be restricting when you want to update them.
// we can also show these in a status if we want to.
const PENDING = 'pending';
const PROCESSING = 'processing';
const PAYMENT_FAILED = 'payment_failed';
const COMPLETED = 'completed';
- php artisan make:migration add_status_to_orders_table --table=orders
- set the boot() method to set our default state pending
php artisan make:controller Orders\\OrderController
- Validation is is very important for a few reasons
- Lets say we create order with an address We need to know that the address for an order belongs to the user Otherwise someone can create order and ship it to any address which is not a great idea
- Second we need to make sure the shipping_method_id we been using is valid as well
- For example if I am from UK I can easily find out the shipping_method_id and switch it over
- We need to check that the shipping method for this order is valid for the address that is being used for this order
- We going to create a basic validation then create a more complex custom validation rule that will work for our valid shipping method
http://cart-api.test/api/orders
- create request to add all of our order validation rules
php artisan make:request Orders\\OrderStoreRequest
- In OrderStoreRequest to get access to currently signed in user
- in FormRequest
class OrderStoreRequest extends FormRequest
we follow it - use Illuminate\Foundation\Http\FormRequest; and see that it extend our base Request
class FormRequest extends Request implements ValidatesWhenResolved
- We can get our user so all we have to do is $this->user()->id
- Error: Laravel action is not authorized to fix it remember to set authorize to true in the request OrderStoreRequest
public function authorize()
{
return true;
}
- In Postman go to Body and add address_id to send make sure is a valid one currently in the database
- php artisan make:test Orders\OrderStoreTest
- We don't test for error message since then it would make our test very fragile when we change error message
- Instead we look for
->assertJsonValidationErrors(['shipping_method_id']);
- php artisan make:rule ValidShippingMethod
php artisan make:migration add_subtotal_to_orders_table --table=orders
test_it_can_create_an_order()
in OrderStoreTest going to be a complex test that is going to need a protected function to work- Create the protected function orderDependencies(User $user)
- In postman send
http://cart-api.test/api/orders
- Send a POST with postman to
http://cart-api.test/api/cart
- Then on Body > raw
{
"products": [
{ "id": 2, "quantity":2 }
]
}
- dd(get_class($cart->products())); get_class is used to not get too much output just the class
use Illuminate\Database\Eloquent\Collection;
- We going to modify and extend the Collection with custom one.
- In ProductVariation create a new newCustomCollection method
- Which will be Custom Collection that extend base laravel collection
- create class that extends collection there called ProductVariationCollection.php
- Now dd(get_class($cart->products())); and see
"App\Models\Collections\ProductVariationCollection"
instead - create a forSyncing method in ProductVariationCollection
- So you can use it here
$order->products()->sync($cart->products()->forSyncing());
php artisan make:test Collections\\ProductVariationCollectionTest --unit
- We don't want order created if they don't have any product attached to them.
- delete tables data a) product_variation_order b) orders c) cart_user
- In postman do GET request to
http://cart-api.test/api/cart
- we also have in Cart the isEmpty method which not only checks if there are no products
- Checks if quantity have been reduce as part of not being available
- So we can use isEmpty again in store method to check if our cart is empty
- Lets try first to check how we can create empty order before we implement the if statement on store method
- in postman send POST request to
http://cart-api.test/api/orders
- It does work but is an useless empty order
https://laravel.com/docs/8.x/events
- Create an event that process the payment first of all and then empty the cart in the OrderController.php store() method
- Go to EventService.php and add the OrderCreated and EmptyCart paths so you can generate them later
php artisan
look for event section- We going to use
event:generate Generate the missing events and listeners based on registration
php artisan event:generate
- add even to OrderController store method
- Send POST request to
http://cart-api.test/api/cart
- Send GET request to
http://cart-api.test/api/cart
See items in cart - Send a POST request to
http://cart-api.test/api/orders
to create order - Send GET request to
http://cart-api.test/api/cart
to see that cart products [] is empty again
php artisan make:resource OrderResource
- We don't have anything in cart so we need to make order with postman
- Send POST to
http://cart-api.test/api/cart
to add item to cart - send POST to
http://cart-api.test/api/orders
- To test things out in store() method in Returning order details
- comment these lines of code
public function store(OrderStoreRequest $request, Cart $cart)
{
// if ($cart->isEmpty()) {
// return response(null, 400);
// }
$order = $this->createOrder($request, $cart);
$order->products()->sync($cart->products()->forSyncing());
// event(new OrderCreated($order));
return new OrderResource($order);
}
- With Postman send a POST request to
http://cart-api.test/api/cart
to add item in cart - Then send POST to
http://cart-api.test/api/orders
to make order multiple times to see the id increment - This way we test we getting the right response back
- This error is happening because we have an empty table once we creating this test
- What causing this in store() method we have an if ($cart->isEmpty()) that prevent ordering from happening if cart is empty
- The problem is the test not actual app.
- After we added the if statement to prevent ordering from happening if cart is empty
- Since previously when we wrote this tes to create an order we were not thinking about that
- So we make sure we have a list of products in our cart with stock
$user->cart()->sync(
$product = $this->productWithStock()
);
- We going to show warning to the user if the item they trying to order suddenly ends up out of stock.
- In postman Add item to your cart by sending Post to
http://cart-api.test/api/cart
- Then send a GET request with postman
http://cart-api.test/api/cart
- Then we see we added id: 4 which is the product_variation_id
- Then go to stocks database and set product_variation_id: 4 quantity to 0
- Then we send GET request with postman
http://cart-api.test/api/cart
which is going to sync our cart - Now we go and see product_variation_order table and see there is quantity of 1 yes we know we don't have that quantity available.
- What we need to do is go to CartController and see what we do is $cart->sync();
- What we going to do is in OrderController.php sync the cart as we sync the order. adding
$cart->sync();
- In postman Add item to your cart by sending Post to
http://cart-api.test/api/cart
- Then send a GET request with postman
http://cart-api.test/api/cart
and see that quantity is -2 so is not working - Now lets delete all in product_variation_order table
- Delete all in orders table
- Now send POST request to
http://cart-api.test/api/orders
now we see it doesn't work and we have "subtotal": "-7500" - The issue seems to be with Cart.php isEmpty()
public function isEmpty()
{
return $this->user->cart->sum('pivot.quantity') === 0;
}
- Instead lets change it to <= 0
public function isEmpty()
{
return $this->user->cart->sum('pivot.quantity') <= 0;
}
- Now run phpunit to see we didn't break anything for changing the code
- Now lets delete all in product_variation_order table
- Delete all in orders table
- We should not create any of these with minus quantity
- In postman Add item to your cart by sending Post to
http://cart-api.test/api/cart
- Then send a GET request with postman
http://cart-api.test/api/cart
- Now we should have quantity of 0
- Now try to make order with
http://cart-api.test/api/orders
and see that you get a Status: 400 Bad Request - In postman Add item to your cart by sending Post to
http://cart-api.test/api/cart
- Then send a GET request with postman
http://cart-api.test/api/cart
- Make sure there is nothing in orders table and
- Then set stop to 1 again to cart and again with
http://cart-api.test/api/orders
- Now since you using $cart->sync() in different places you don't want to have it in controller
- Instead extact it to middleware
php artisan make:middleware Cart\\Sync
- Add middleware to HTTP/Kernel.php
protected $routeMiddleware = [
'cart.sync' => \App\Http\Middleware\Cart\Sync::class,
];
- Then add the cart.sync to the middleware in OrderController
public function __construct()
{
$this->middleware(['auth:api', 'cart.sync']);
}
Tests\Feature\Orders\OrderStoreTest::test_it_fails_if_not_authenticated
test failing because in- cart.sync here
public function __construct()
{
$this->middleware(['auth:api', 'cart.sync']);
}
- This test is failing because we putting it out of the container
public function __construct(Cart $cart)
{
$this->cart = $cart;
}
- What we do is go to our AppServiceProviders.php when we register the cart return null value
public function register()
{
// now we can always have it in our container
$this->app->singleton(Cart::class, function ($app) {
if (!$app->auth->user()) {
return null;
}
$app->auth->user()->load([
'cart.stock'
]);
return new Cart($app->auth->user());
});
}
php artisan make:middleware Cart\\ResponseIfEmpty
- It will replace store() method if statement that check if cart is empty
if ($cart->isEmpty()) {
return response(null, 400);
}
- Now we can remove it and add in ResponseIfEmpty middleware
public function handle(Request $request, Closure $next)
{
if ($this->cart->isEmpty()) {
return response()->json([
'message' => 'Cart is empty'
], 400);
}
}
- We have issue since middleware is being run before we hit the store method
- To fix this in tests that fails you need to add
$user->cart()->sync(
$product = $this->productWithStock()
);
- Create index method in OrderController.php to grab the order to show it
- We have problem that 'cart.sync', 'cart.isnotempty' should not apply to this method
- So we can use only so that the middleware only apply to store() method
$this->middleware(['cart.sync', 'cart.isnotempty'])->only('store');
- Send Get request with postman to
http://cart-api.test/api/orders
php artisan make:test Orders\\OrderIndexTest
- Test test_it_orders_by_the_latest_first()
- test_it_has_pagination()
- In Postman send Get Request to
http://cart-api.test/api/orders
- We modify "subtotal:" by using to "subtotal": {},
- Using
public function getSubtotalAttribute($subtotal)
- Send Post Request to
http://cart-api.test/api/orders
- Send post request to
http://cart-api.test/api/orders
with postman - Send post request to
http://cart-api.test/api/orders?_debug
with postman to see the rp_statements of laravel debugbar - "nb_statements": 47 which is a lot of queries and we should reduce amount
- We can start reducing them in OrderController.php
$orders = $request->user()->orders()
->with([
'products',
'address',
'shippingMethod'
])
->latest()
->paginate(10);
- We also load products.product the main product of product variation to reduce queries
->with([
'products',
'products.product',
'address',
'shippingMethod'
])
- "nb_statements": 23, queries got reduced with this change.
- add new order and product_variation_order to database and see that "nb_statements": 25 does increase
- What we can do is do a search for "sql" in Postman to see what look like is being query too much.
- We see product_variation is appearing too much so we add
'products.product.variations',
->with([
'products',
'products.product',
'products.product.variations',
'address',
'shippingMethod'
])
- Yet seems it didn't reduce nb_statements": 25
- Now we add
'products.product.variations.stock',
we can find stock in ProductVariation.php methods it reduced and nb_statements": 20 - We also going to ass
products.type
as well and it reduced "nb_statements": 16 queries again. - Now we search for "sql" again
"sql": "select \"product_variations\".*, \"product_variation_stock_view\".\"product_variation_id\" as \"pivot_product_variation_id\", \"product_variation_stock_view\".\"stock\" as \"pivot_stock\", \"product_variation_stock_view\".\"in_stock\" as \"pivot_in_stock\" from \"product_variations\" inner join \"product_variation_stock_view\" on \"product_variations\".\"id\" = \"product_variation_stock_view\".\"product_variation_id\" where \"product_variation_stock_view\".\"product_variation_id\" = 8",
"sql": "select \"product_variations\".*, \"product_variation_stock_view\".\"product_variation_id\" as \"pivot_product_variation_id\", \"product_variation_stock_view\".\"stock\" as \"pivot_stock\", \"product_variation_stock_view\".\"in_stock\" as \"pivot_in_stock\" from \"product_variations\" inner join \"product_variation_stock_view\" on \"product_variations\".\"id\" = \"product_variation_stock_view\".\"product_variation_id\" where \"product_variation_stock_view\".\"product_variation_id\" = 8",
"sql": "select \"product_variations\".*, \"product_variation_stock_view\".\"product_variation_id\" as \"pivot_product_variation_id\", \"product_variation_stock_view\".\"stock\" as \"pivot_stock\", \"product_variation_stock_view\".\"in_stock\" as \"pivot_in_stock\" from \"product_variations\" inner join \"product_variation_stock_view\" on \"product_variations\".\"id\" = \"product_variation_stock_view\".\"product_variation_id\" where \"product_variation_stock_view\".\"product_variation_id\" = 8",
- So we add
'products.stock'
and reduced again "nb_statements": 12 - Now we can remove one of the orders and product_variation_order
- And we should see the exact same result
- If you have more than one product, and product 1 changes we set $this->changed = true but is product 2 comes after that and have not changed It will always change $this->change = false, so if first product changes you not going to be alerted.
- We going to modify tests to help us fix this issue
- Test will detect issue n test_it_can_check_if_the_cart_has_changed_after_syncing()
$user->cart()->attach([
$product->id => [
'quantity' => 2
],
$anotherProduct->id => [
'quantity' => 0
],
]);
- Now to fix issue we use if statement in Cart.php sync() method
- Change this
$this->changed = $quantity != $product->pivot->quantity;
- to this which will now only change value if this evaluate to true.
if ($quantity != $product->pivot->quantity) {
$this->changed = true;
}
-
Going to be similar to addresses
-
php artisan make:model PaymentMethod -m
-
php artisan make:factory PaymentMethodFactory
-
php artisan make:test Models\PaymentMethods\PaymentMethodTest --unit
-
We found problem that in migration we have unique index set for provider id, since we got 2 payment method they going to conflict and get error
Illuminate\Database\QueryException : SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "payment_methods_provider_id_unique"
DETAIL: Key (provider_id)=(abc) already exists. (SQL: insert into "payment_methods" ("card_type", "last_four", "provider_id", "default", "user_id", "updated_at", "created_at")
values (Visa, 4242, abc, 1, 1, 2020-12-30 01:57:09, 2020-12-30 01:57:09) returning "id")
- the issue is that we hardcoded in the PaymentMethodFactory
public function definition()
{
return [
'card_type' => 'Visa',
'last_four' => '4242',
'provider_id' => 'abc',
];
}
- So we create random provider_id so that the id is unique instead of static
public function definition()
{
return [
'card_type' => 'Visa',
'last_four' => '4242',
'provider_id' => str_random(10),
];
}
- In Address model we going to make this dynamic
static::creating(function ($address) {
// dd($address);
// If the user already has default address set the other ones defaults to false after
if ($address->default) {
$address->user->addresses()->update([
'default' => false
]);
}
});
- by using newQuery
static::creating(function ($address) {
if ($address->default) {
$address->newQuery()->where('user_id', $address->user->id)->update([
'default' => false
]);
}
});
- php artisan make:controller PaymentMethods\PaymentMethodController
- Create route in api.php
Route::resource('paymentMethods', PaymentMethodController::class);
- Check in Postman to see if route is working by sending get request to
http://cart-api.test/api/payment-methods
- Do a dd() in PaymentMethodController.php index method and create 2 payment methods in database
public function index(Request $request)
{
dd($request->user()->paymentMethods);
}
- Now send Get Request to Postman to see the information
http://cart-api.test/api/payment-methods
- Now make resource to display that data php artisan make:resource PaymentMethodResource
- Then use the PaymentMethodResource in the index method to display data you specified.
public function index(Request $request)
{
// dd($request->user()->paymentMethods);
return PaymentMethodResource::collection(
$request->user()->paymentMethods
);
}
- Now send Get Request to Postman to see the information
http://cart-api.test/api/payment-methods
{
"data": [
{
"id": 1,
"card_type": "Visa",
"last_four": "4242",
"default": true
},
{
"id": 2,
"card_type": "Mastercard",
"last_four": "1234",
"default": false
}
]
}```
9. Now we have the information we get to display on the front end.
10. Now we add some authentication to PaymentMethodController.php
```php
public function __construct()
{
$this->middleware(['auth:api']);
}
- Then test if it authenticated with postman by removing bearer token
- php artisan make:test PaymentMethods\PaymentMethodIndexTest
- php artisan make:migration add_payment_method_id_to_orders_table --table=orders
- We can check if the payment method belong to current user in OrderStoreRequest.php
'payment_method_id' => [ 'required', Rule::exists('payment_methods', 'id')->where(function ($builder) { $builder->where('user_id', $this->user()->id); })
],
3. In Postman send GET request to `http://cart-api.test/api/cart` we don't have product
4. Lets add product to cart with Postman POST request `http://cart-api.test/api/cart`
5. login with `http://cart-api.test/api/auth/login` if you get error to get bearer token
6. Send GET request with Postman to see now we have item in cart `http://cart-api.test/api/cart`
7. Now Try to create order with POST request in postman to `http://cart-api.test/api/orders`
8. Make sure that there is stock or you going to end up putting quantity 0 in cart, and not be able to make order.
9. and you will see the new validation rule is working properly
```json
{
"message": "The given data was invalid.",
"errors": {
"payment_method_id": [
"The payment method id field is required."
]
}
}
- Try to create order with POST request in postman to
http://cart-api.test/api/orders
and add payment_method_id with another user id - Will give validation error
- Now use currently logged in id and it should work
- Since we added payment_method_id the test
test_it_can_create_an_order()
should fail. - So we need to now modify $this->oderDependencies and modify the test
- Now we need to modify every test that relies on
$this->oderDependencies
- We get multiple errors since OrderFactory doesn't generate payment_mehod_id
- Still get error since we forget to assign user_id to payment_method_id
- https://github.com/stripe/stripe-php
composer require stripe/stripe-php
- in stripe.com create new store
- Grab the keys from Developers > Api keys https://dashboard.stripe.com/test/apikeys
- go to config/services.php to see where to add the Key
'stripe' => [
'secret' => env('STRIPE_SECRET')
]
- Now we can access it using stripe.secret
- We can add it on boot in AppServiceProvider.php
public function boot()
{
Stripe::setApiKey(config('services.stripe.secret'));
}
- do a phpunit to check changes didn't break anything
- This will be collection of methods in 1 class.
- Which will allow us to create customer, add a cart, charge user with their default payment method
- This will be tied down to payment provider we are using
- With Postman send a POST request to
http://cart-api.test/api/payment-methods
to see what is missing - We can use AppServiceProvider to declare Gateway which we can use to switch payment method from Stripe to another.
- Now we can verify if interface working by sending POST request to
http://cart-api.test/api/payment-methods
"message": "Class App\\Cart\\Payments\\Gateways\\StripeGateway contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (App\\Cart\\Payments\\Gateway::withUser, App\\Cart\\Payments\\Gateway::createCustomer)",
- PaymentMethodController.php store() method remains the same regardless how we switch gateway
- If you later want to implement braintree you just have to implement
a) BraintreeGateway.php
b) BraintreeCustomer.php
- We need to return $this to be able to chain any other method. in app /Cart/Payments/Gateways/StripeGateway.php withUser method!
- In stripeGateway.php
public function createCustomer()
{
return new StripeGatewayCustomer();
}
- If where you will see that we connect stripeGateway.php to StripeGatewayCustomer.php
- At the moment we don't want to add card yet in PaymentMethodController.php
public function store(Request $request)
{
$card = $this->gateway->withUser($request->user())
->createCustomer();
dd($card);
// ->addCard($request->token);
}
-
php artisan make:migration add_gateway_customer_id_to_users_table --table=users
-
Do a dd($customer) to see what happens.
public function createCustomer()
{
// we will implement method that will get customer from stripe, based on the user gateway_customer_id
if ($this->user->gateway_customer_id) {
return 'customer';
}
// create stripe customer here and return that as part of our stripe gateway customer object.
$customer = $this->createStripeCustomer();
dd($customer);
return new StripeGatewayCustomer();
}
- Now send a POST request to
http://cart-api.test/api/payment-methods
using POSTMAN - Now you can see we get a customer back from stripe
- You can also use Stripe dashboard to see this new created customer
- Send POST request to
http://cart-api.test/api/payment-methods
- After adding id to database you will get error
"message": "Call to a member function addCard() on string",
- You can check customer id here
https://dashboard.stripe.com/test/customers/cus_IgUKHnTDgrYld1
writing the customer id in the end the one in user table cart database - We need to truncate and delete all files from payment_methods
- Right click on payment_methods table, that needs to be truncated in Dbeaver Database Navigator, choose Tools->Truncate
- Click on Restart Identity and Cascade click 'Start'. That's all.
- Send POST request to
http://cart-api.test/api/payment-methods
we should get null since we are using dd() and method not returning information - Now check Stripe Dashboard for new customer
https://dashboard.stripe.com/test/customers/cus_Igkt5xUNbyv9R3
- Then click on card and see the
ID card_1I5NO2HCos07RG12lTRiDHf6
match the card id in payment_methods table and compare it with the provider_id - There is a problem that each card added is being added as default true, it should only be last card.
- To fix this go to StripeGateWayCustomer.php create method and add 'default' => true
$this->gateway->user()->paymentMethods()->create([
'provider_id' => $card->id,
'card_type' => $card->brand,
'last_four' => $card->last4,
'default' => true
]);
Each time you make changes truncate payment_methods and in users table delete the gateway_customer_id 15. Now the last card added will be default one so lets get provider_id from the default card and check dashboard to see if it set as default.
- In StripeGatewayCustomer.php addCard() method return the data
return $this->gateway->user()->paymentMethods()->create([
'provider_id' => $card->id,
'card_type' => $card->brand,
'last_four' => $card->last4,
'default' => true
]);
- Then send a Post Request with Postman to http://cart-api.test/api/payment-methods
- Now on PaymentMethodController.php store() method
return new PaymentMethodResource($card);
- and you will receive data to display on the site.
{
"data": {
"id": 4,
"card_type": "Visa",
"last_four": "4242",
"default": true
}
}
php artisan make:test PaymentMethods\\PaymentMethodStoreTest
- Test failed
Failed to find a validation error in the response for key: 'token'
- Since we needed to add Validation in PaymentMethodController.php
public function store(Request $request)
{
$this->validate($request, [
'token' => 'required'
]);
}
- Is better to deal with API rather than deal with Fake data through mocking.
- Only problem is if Stripe API is temporally down and you need internet connection to test it
- php artisan make:request PaymentMethods\PaymentMethodRequest
- Go to POSTMAN and send a Get request to cart
http://cart-api.test/api/cart
- send a POST request to add item to cart
http://cart-api.test/api/cart
- Make an order with Postman by send POST request to
http://cart-api.test/api/orders
- Then check on https://dashboard.stripe.com/test/payments?status%5B%5D=successful
- php artisan
- php artisan make:exception PaymentFailedException
- php artisan make:event Orders\OrderPaymentFailed
- Send a POST & GET request with Postman in
http://cart-api.test/api/cart
to add product to cart and see item added. - If stock is 0 make sure to add stock in stocks table on the cart database.
- Send POST request to
http://cart-api.test/api/orders
with postman - Now check database for orders table to see order status as payment_failed
- We checked the Exception worked by adding it in the charge method of StripeGatewayCustomer.php
public function charge(PaymentMethod $card, $amount)
{
try {
// throw new PaymentFailedException();
- Send a POST & GET request with Postman in
http://cart-api.test/api/cart
to add product to cart and see item added. - Send POST request to
http://cart-api.test/api/orders
with postman
- Use
dd($response->getContent());
intest_it_empties_the_cart_when_ordering()
to debug - Then run
test_it_empties_the_cart_when_ordering()
to see what is the actual problem "message": "Could not determine which URL to request: Stripe\\Customer instance has invalid ID:
- The issue is happening in ProcessPayment.php in handle method since
- We are sending in the withUser() to grab the getCustomer() instance
- We can't grab the customer since we don't have the id associated with that customer on the test
test_it_empties_the_cart_when_ordering()
- What we going to do is update the orderDependencies() method in OrderStoreTest.php
- So we going to update user with real account Stripe id
protected function orderDependencies(User $user)
{
$stripeCustomer = \Stripe\Customer::create([
'email' => $user->email,
]);
$user->update([
'gateway_customer_id' => $stripeCustomer->id
]);
- Had an issue where app/Events/Order/OrderPaymentFailed.php namespace was incorrect.
- Was able to find error using
dd($response->getContent());
intest_it_empties_the_cart_when_ordering()
to debug - Be aware we're not yet testing that payment is passing we will work on it next.
- Since these going to be unit test we not going to hit the stripe api so we going to use mockery data
- php artisan make:test Listeners\EmptyCartListenerTest --unit
- In EmptyCart.php you can check that the test from EmptyCartListenerTest.php test_it_should_clear_the_cart() is working by commenting
$this->cart->empty();
public function handle()
{
// $this->cart->empty();
}
- What we plan to test in ProcessPayment.php
public function handle(OrderCreated $event)
{
$order = $event->order;
try {
$this->gateway->withUser($order->user)
->getCustomer()
->charge(
// 1# Test that order successfully charge and pass through correct paymentMethod
// 2# correct total for the order
$order->paymentMethod, $order->total()->amount()
);
// 3# Test it fires the OrderPaid event
event(new OrderPaid($order));
} catch (PaymentFailedException $e) {
// 4# Test if event happens if order fails
event(new OrderPaymentFailed($order));
}
}
- We going to test all of these without hitting Stripe
- We not Mocking Stripe but Mock Payment Gateway
php artisan make:test Listeners\\ProcessPaymentListenerTest --unit
- php artisan make:model Transaction -m
- Has many so you can setup up in future so that user can may more or pay half
- HasMany makes things easier to work with.
public function transactions()
{
return $this->hasMany(Transaction::class);
}
- php artisan make:factory TransactionFactory
- There was an error with test
public function test_it_has_many_transactions()
Undefined column: 7 ERROR: column "amount" of relation "transactions" does not exist
- Problem was that in our TransactionFactory we wrote amount instead of total
- Now that we have transaction in place.
- Once we had OrderPaid and fire the event. We can go ahead and create that transaction.
- In
app/Providers/EventServiceProvider.php
- add CreateTransaction::class so that it runs when OrderPaid::class runs.
protected $listen = [
OrderPaid::class => [
CreateTransaction::class,
MarkOrderProcessing::class
],
];
- Then add
use App\Listeners\Order\CreateTransaction;
- Run
php artisan event:generate
- It should generate
app/Listeners/Order/CreateTransaction.php
- Then we can get the information from the event
public function handle(OrderPaid $event)
{
$event->order->transactions()->create([
'total' => $event->order->total()->amount() // 1000
]);
}
- Now to test if this works properly we can use Postman
- Login to get Token by sending GET request to
http://cart-api.test/api/auth/login
using POSTMAN and copy the token - Now in POSTMAN send GET request to see what items in your cart
http://cart-api.test/api/cart
and if empty then - Send POSTMAN send POST request to add item to cart
http://cart-api.test/api/cart
- In POSTMAN send GET request to see what items in your cart
http://cart-api.test/api/cart
- Now make an Order by sending with POSTMAN a POST request to
http://cart-api.test/api/orders
- Now Check cart transactions table has an order inside
- Copy MarkOrderPaymentListenerTest.php to save time and create CreateTransactionListenerTest.php
- You can add more information that you need to show in a transaction after processing payments.