Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 19: Checkout

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

In this article we will start describing the flow of the checkout process and how to handle payments using two types of payment methods which is to pay on delivery and pay using paypal.

 

 

 

You might remember when when have worked on the cart the last thing is displaying the cart items, and lift with a checkout button at the bottom of the page, usually when the user clicks on that button it will be redirected to a checkout screen where he can select a shipping address and payment method, this screen similar to the screen in the below figure:

 

As shown in this figure the screen have these items:

  • The shipping address or the shipping address marked as primary.
  • The cart summary
  • Payment methods section

A typical checkout process will take this flow:

  • The user selects a shipping address.
  • Then there are two payment options, whether to select paypal or select pay on delivery.
  • If the user selects the paypal type then we will display the paypal smart buttons so that the user can pay using paypal dedicated window. In this window the user has the option to proceed with the payment or cancel it.
  • If the user select pay on delivery then another button will be shown at the bottom, if the user clicks on this button it will send the order to the system admin with a pending status, meaning that the system admin will update this status once the order is delivered to the customer or not.

There are many ways to handle payment with paypal, the first option using paypal restful apis through server side. While the other option using paypal express paypal or paypal smart button. In this tutorial i will use the smart buttons approach which is a javascript code that you embed in the page and it will display a nice paypal button to click on it to pay, you can read this article describing how to use it.

 

Overview of Paypal Payments

When paying with paypal there to types of accounts, personal account and business account. The personal account used when you have to shop online or to pay using paypal for example shopping from any e-commerce platform meaning that this account type doesn’t accept payments(pay only). The business account (or merchant account) in the other hand accept online payments so the typical use case in the e-commerce platform you are the owner and sell products and accept payments from customers.

Now you understand the main two types of paypal accounts, let’s move to handle paypal payments. To achieve that paypal provides us a sandbox account (or developer account) which is useful to make a fake payments in development phase, once you published your website into production you can switch this account into a live account.

Note that when testing payments you will need two sandbox accounts one for the buyer and one for the seller. Registering a sandbox account is simple by going into this url. At first register a seller account, this account will be used to accept payments, next logout and register another account for the buyer.

Once you create these accounts login with the seller account and go to paypal developer dashboard. You will be redirected to paypal applications screen as shown in this figure:

Building Ecommerce Website With Lumen Laravel And Nuxtjs - paypal apps

As shown click on the create app button to create a new application, give your application a name and click Create App. Once this done grab the Client ID and Secret as shown in this figure:

Also In Sandbox App Settings ensure that these settings is selected as shown in this figure:

 

Update online-shop-frontend/nuxt.config.js and add the paypal Client ID after the this line:

buildDir: '.nuxt/frontend',

Add this env variable:

env: {
    PAYPAL_CLIENT_ID: "Your Client ID"   // add your client ID that you created in the previous step
  }

We will use this env variable when working on the checkout page to pass this variable to paypal script which in turn tells paypal to authorize payments on this user account.

To learn more about paypal smart buttons click on this link.

 

 

Payment Methods & Orders Api

Let’s return to the backend in Lumen and create two controllers one for the payment methods and another for the orders.

app/Http/Controllers/PaymentMethodsController.php

<?php


namespace App\Http\Controllers;


use App\Models\PaymentMethod;

class PaymentMethodsController extends Controller
{
    public function index()
    {
        return response()->json(['payment_methods' => PaymentMethod::all()]);
    }
}

app/Http/Controllers/OrdersController.php

<?php


namespace App\Http\Controllers;



use App\Models\Order;
use App\Models\OrderDetail;
use App\Models\PaymentMethod;
use App\Models\Product;
use App\Models\ShoppingCart;
use App\Traits\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class OrdersController extends Controller
{
    use Helpers;

    public function __construct()
    {
        $this->middleware('super_admin_check:show-update-getLatestPendingOrders');
    }

    public function index(Request $request)
    {
        
    }

    public function store(Request $request)
    {
        
    }

    public function show($id)
    {
        
    }

    public function update(Request $request, $id)
    {
        
    }

    public function getLatestPendingOrders()
    {
        
    }
}

The OrdersController is just a blueprint i will update it later. Now let’s update routes/web.php

routes/web.php below this block:

$router->group(['prefix' => 'shippingAddress'], function () use ($router) {
....
});

Add this route groups:

$router->group(['prefix' => 'paymentMethods'], function () use ($router) {
           $router->get('/', 'PaymentMethodsController@index');
        });
$router->group(['prefix' => 'orders'], function () use($router) {
$router->get('/', 'OrdersController@index');
$router->post('/', 'OrdersController@store');
$router->get('/latest-pending-orders', 'OrdersController@getLatestPendingOrders');
$router->get('/{id}', 'OrdersController@show');
$router->put('/{id}', 'OrdersController@update');
});

That’s it now, let’s add these apis into the Nuxt project. In the Nuxt project go to the api/ directory and create payment-method.js and order.js

api/payment-method.js

const PaymentMethodsApi = {
  setAuthToken: (axios) => {
    axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));
  },
  list: (axios) => {
    PaymentMethodsApi.setAuthToken(axios);
    return axios.$get('/api/paymentMethods');
  }
}

export {PaymentMethodsApi};

api/order.js

const OrdersApi = {
  setAuthToken: (axios) => {
    axios.setHeader('Authorization', "Bearer " + localStorage.getItem('auth_token'));
  },
  store: (axios, payload) => {
    OrdersApi.setAuthToken(axios);
    return axios.$post('/api/orders', payload);
  },
  list: (axios) => {
    OrdersApi.setAuthToken(axios);
    return axios.$get('/api/orders');
  }
}

export {OrdersApi};

 

Preparing Checkout Page

We will modify the checkout page layout according to the figure described earlier so open pages/checkout.vue and add this code:

<template>
  <section id="cart_items">
    <div class="container">

      <div class="step-one">
        <h2 class="heading" style="padding-left: 5px !important;">Step1: Choose Shipping</h2>
      </div>

      <!-- display shipping addresses here -->

      <div class="review-payment">
        <h2>Review Cart</h2>
      </div>

      <div class="table-responsive cart_info">
        <table class="table table-condensed">
          <thead>
          <tr class="cart_menu">
            <td class="image" width="15%">Item</td>
            <td class="description" width="35%"></td>
            <td class="price" width="10%">Price</td>
            <td class="quantity" width="15%">Quantity</td>
            <td class="total" width="10%">Total</td>
          </tr>
          </thead>
          <tbody>
            <tr v-for="item in this.cart" v-bind:key="item.id">
              <td class="cart_product">
                <nuxt-link :to="'/p/' + item.product_id + '/' + item.product.slug"><img :src="item.product.gallery[0].image_url.cart_thumb" v-bind:alt="item.title"></nuxt-link>
              </td>
              <td class="cart_description" width="30%">
                <h4><nuxt-link :to="'/p/' + item.product_id + '/' + item.product.slug"><span>{{ item.product.title_short }}</span></nuxt-link></h4>
                <p v-if="item.product.product_code != ''">Product Code: {{item.product.product_code}}</p>
              </td>
              <td class="cart_price">
                <p>${{ item.product.price_after_discount }}</p>
              </td>
              <td class="cart_quantity">
                <div class="cart_quantity_button">
                  <input class="cart_quantity_input" type="text" name="quantity" :value="item.amount_temp" autocomplete="off" size="2" disabled />
                </div>
              </td>
              <td class="cart_total">
                <p class="cart_total_price">${{ item.total_price_formatted }}</p>
              </td>
            </tr>


            <tr>
              <td colspan="3">&nbsp;</td>
              <td colspan="2">
                <table class="table table-condensed total-result">
                  <tbody>
                    <tr>
                      <td>Total</td>
                      <td><span>${{this.getCartTotal()}}</span></td>
                    </tr>
                  </tbody>
                </table>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <div class="step-one">
        <h2 class="heading" style="padding-left: 5px !important;">Step2: Choose Payment</h2>
      </div>

      <!-- display payment methods here -->
      
      <a href="#" class="btn btn-lg btn-default check_out pull-right" :style="'margin-bottom: 20px;display:'">Proceed</a>

      <Paypal></Paypal>
    </div>
  </section>
</template>

<script>
    import Paypal from "../components/checkout-components/Paypal";

    export default {
       name: "Checkout",
      components: {Paypal},
      middleware: "auth",
       data() {
          return {
            
          }
       },
       head() {
        return {
          title: 'Online Shop | Checkout',
          meta: [
            {
              hid: 'description',
              name: 'description',
              content: 'Checkout Page'
            }
          ],
          script: [
            {
              type: 'text/javascript',
              src: "https://www.paypal.com/sdk/js?client-id="+process.env.PAYPAL_CLIENT_ID+"&currency=USD&disable-funding=credit,card"
            }
          ]
        }
      },
      computed: {
        cart() {
          return this.$store.state.cart.cart;
        }
      },
      methods: {
        getCartTotal() {
          let total = 0;
          this.$store.state.cart.cart.map(item => {
            total += item.total_price_numeric;
          });

          return total.toFixed(1);
        }
      },
      mounted() {
        if (this.cart.length == 0) {
          alert("You must have items in the cart in order to checkout!");
          this.$router.push("/");
        }

     
      }
    }
</script>

<style scoped>
  .shipping-addresses label {
    font-weight: normal !important;
  }

  .payment-options {
    margin-bottom: 20px !important;
  }
</style>

Next create this partial component <Paypal />, create a new directory checkout-components/ inside components/ directory and then add Paypal.vue

components/checkout-components/Paypal.vue

<template>
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <i class="fa fa-spinner fa-spin" style="font-size: 33px; margin-left: 46%" v-if="show_loading"></i>
      <div id="paypal-button-container"></div>
    </div>
  </div>
</template>

<script>
    import {OrdersApi} from "../../api/order";

    export default {
        name: "Paypal",
        props: [],
        data() {
          return {
            show_loading: false
          }
        },
        methods: {
         
        },
        mounted() {
          
          // initialize paypal buttons
          setTimeout(() => {
            paypal.Buttons({
              createOrder: (data, actions) => {
                return actions.order.create({
                  prefer: 'return=representation',
                  purchase_units: [{
                    description: 'Purchasing items from online shop e-commerce app',
                    
                  
                  }]
                });
              },
              onApprove: (data, actions) => {
                
              },
              onCancel: (data) => {
                
              },
              onError: (err) => {
                alert(err);
              }
            }).render('#paypal-button-container');
          }, 1000);
        }
    }
</script>

As you see in the above code the checkout page mounted() hook i check if the cart is not empty otherwise we redirect the user to homepage. Also i am displaying the cart summary, the shipping addresses and payment methods not shown for now we will display them below.

In the head() hook we included the paypal SDK script, along with the client-id , i retrieve the client-id from the env using process.env and i set the default currency as USD.

The partial component <Paypal /> contains the paypal widget that will be displayed if we selected paypal payment method.

To display the paypal smart buttons you have to call paypal.Buttons() method passing it an object that accept these callbacks (createOrder, onApprove, onCancel, onError), the createOrder() initialize and sends the order items to paypal, onApprove() fires if the order approved by paypal so you can update the database with successful payment, onCancel() fires if the user cancelled the order and onError() fires if there is internal error happened during payment.

 

Now let’s update the page code to include the shipping addresses and payment methods.

pages/checkout.vue

<template>
  <section id="cart_items">
    <div class="container">

      <div class="step-one">
        <h2 class="heading" style="padding-left: 5px !important;">Step1: Choose Shipping</h2>
      </div>

      <p class="alert alert-info" v-if="!this.shipping_addresses.length">No shipping addresses! <nuxt-link to="/shipping-addresses">Add Shipping Address</nuxt-link> </p>

      <div class="shipping-addresses" v-if="this.shipping_addresses.length">
        <ul class="nav">
          <li v-for="address in this.shipping_addresses" :key="address.id" style="display: block">
            <label><input type="radio" name="shipping_address_id" :value="address.id" v-model="selected_shipping_address">
              <span>
                <strong>{{ address.country }} - {{ address.city }} - ({{ address.postal_code }}) - {{address.address}} (Primary)</strong>
                <span>{{address.mobile}}</span>
              </span>
            </label>
          </li>
        </ul>
      </div>

      <div class="review-payment">
        <h2>Review Cart</h2>
      </div>

      <div class="table-responsive cart_info">
        <table class="table table-condensed">
          <thead>
          <tr class="cart_menu">
            <td class="image" width="15%">Item</td>
            <td class="description" width="35%"></td>
            <td class="price" width="10%">Price</td>
            <td class="quantity" width="15%">Quantity</td>
            <td class="total" width="10%">Total</td>
          </tr>
          </thead>
          <tbody>
            <tr v-for="item in this.cart" v-bind:key="item.id">
              <td class="cart_product">
                <nuxt-link :to="'/p/' + item.product_id + '/' + item.product.slug"><img :src="item.product.gallery[0].image_url.cart_thumb" v-bind:alt="item.title"></nuxt-link>
              </td>
              <td class="cart_description" width="30%">
                <h4><nuxt-link :to="'/p/' + item.product_id + '/' + item.product.slug"><span>{{ item.product.title_short }}</span></nuxt-link></h4>
                <p v-if="item.product.product_code != ''">Product Code: {{item.product.product_code}}</p>
              </td>
              <td class="cart_price">
                <p>${{ item.product.price_after_discount }}</p>
              </td>
              <td class="cart_quantity">
                <div class="cart_quantity_button">
                  <input class="cart_quantity_input" type="text" name="quantity" :value="item.amount_temp" autocomplete="off" size="2" disabled />
                </div>
              </td>
              <td class="cart_total">
                <p class="cart_total_price">${{ item.total_price_formatted }}</p>
              </td>
            </tr>


            <tr>
              <td colspan="3">&nbsp;</td>
              <td colspan="2">
                <table class="table table-condensed total-result">
                  <tbody>
                    <tr>
                      <td>Total</td>
                      <td><span>${{this.getCartTotal()}}</span></td>
                    </tr>
                  </tbody>
                </table>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <div class="step-one">
        <h2 class="heading" style="padding-left: 5px !important;">Step2: Choose Payment</h2>
      </div>

      <div class="payment-options">
                    <span v-for="method in this.payment_methods" :key="method.id">
                        <label><input type="radio" name="payment_method" :value="method.slug" v-model="selected_payment_method" @change="toggleButton()"> {{ method.name }}</label>
                    </span>
      </div>
      <a href="#" class="btn btn-lg btn-default check_out pull-right" :style="'margin-bottom: 20px;display:' + (this.showButton ? 'block' : 'none') " @click.prevent="payCash()">Proceed</a>

      <Paypal v-if="this.selected_payment_method == 'paypal' && this.selected_shipping_address != ''" :cart="this.cart" :shipping-addresses="this.shipping_addresses" :shipping-address="this.selected_shipping_address" :payment-method="this.selected_payment_method"></Paypal>
    </div>
  </section>
</template>

<script>
    import {ShippingAddressApi} from "../api/shipping-address";
    import {PaymentMethodsApi} from "../api/payment-method";
    import Paypal from "../components/checkout-components/Paypal";
    import {OrdersApi} from "../api/order";

    export default {
       name: "Checkout",
      components: {Paypal},
      middleware: "auth",
       data() {
          return {
            shipping_addresses: [],
            payment_methods: [],
            selected_shipping_address: "",
            selected_payment_method: "",
            showButton: false
          }
       },
       head() {
        return {
          title: 'Online Shop | Checkout',
          meta: [
            {
              hid: 'description',
              name: 'description',
              content: 'Checkout Page'
            }
          ],
          script: [
            {
              type: 'text/javascript',
              src: "https://www.paypal.com/sdk/js?client-id="+process.env.PAYPAL_CLIENT_ID+"&currency=USD&disable-funding=credit,card"
            }
          ]
        }
      },
      computed: {
        cart() {
          return this.$store.state.cart.cart;
        }
      },
      methods: {
        toggleButton() {
          if(this.selected_payment_method !== 'paypal') {
            this.showButton = true;
          } else {
            this.showButton = false;
          }
        },
        getCartTotal() {
          let total = 0;
          this.$store.state.cart.cart.map(item => {
            total += item.total_price_numeric;
          });

          return total.toFixed(1);
        },
        payCash() {
          
        }
      },
      mounted() {
        if (this.cart.length == 0) {
          alert("You must have items in the cart in order to checkout!");
          this.$router.push("/");
        }

        ShippingAddressApi.list(this.$axios).then(response => {
           this.shipping_addresses = response.shipping_addresses.filter(item => item.is_primary == 1 );
        });

        PaymentMethodsApi.list(this.$axios).then(response => {
           this.payment_methods = response.payment_methods;
        });
      }
    }
</script>

<style scoped>
  .shipping-addresses label {
    font-weight: normal !important;
  }

  .payment-options {
    margin-bottom: 20px !important;
  }
</style>

The shipping address and payment methods is shown. To check the paypal widget try to select a shipping address and the paypal checkbox you will see the paypal smart buttons.

The proceed button will be shown only if we the payment method is not paypal so i have added the toggleButton() method to toggle the button display. When the button is clicked i attached a payCash() method which is empty right now, this method will call the OrdersApi.store() method to send the order to the backend with a pending state.

 

Saving The Order

At this point the proceed button doesn’t do something we have to update it to save the order, so make these updates to the checkout page:

Update the data() method:

data() {
          return {
            shipping_addresses: [],
            payment_methods: [],
            selected_shipping_address: "",
            selected_payment_method: "",
            error_message: "",
            validation_errors: [],
            showButton: false
          }
       }

The payCash() method:

payCash() {
          this.error_message = "";
          this.validation_errors = [];

          if (this.cart.length == 0) {
            alert("You must have items in the cart in order to checkout!");
            return;
          }

          if (this.shipping_addresses.length == 0) {
            alert("You must have at least one main shipping address");
            return;
          }

          if (!this.selected_shipping_address) {
            alert("Please select shipping address");
            return;
          }

          if (!this.selected_payment_method) {
            alert("Please select payment method");
            return;
          }

          // skip for paypal
          if(this.selected_payment_method == 'paypal') {
            return;
          }

          if (confirm("Confirm sending the payment request?")) {
            const data = {
              shipping_address_id: this.selected_shipping_address,
              payment_method: this.selected_payment_method,
              status_message: "Order added and waiting for delivery"
            };

            OrdersApi.store(this.$axios, data).then(response => {

              this.$store.commit('cart/clear');

              this.$router.push("/checkout-thanks");
            }).catch(err => {
              this.error_message = err.response.data.message;
              if (err.response.data.errors) {
                let errors = [];
                for (let key in err.response.data.errors) {
                  errors.push(err.response.data.errors[key][0]);
                }

                this.validation_errors = errors;
              }
            });
          }
        }

Update the template add those lines above <div class=”step-one”>

<div class="alert alert-danger" v-if="this.error_message != ''">{{ this.error_message }}</div>

      <ul class="sufee-alert alert with-close alert-danger alert-dismissible" v-if="this.validation_errors.length">
        <li v-for="(error, index) in this.validation_errors" :key="index">{{ error }}</li>
      </ul>

The payCash() method does multiple checks in the beginning and then calls OrdersApi.Store() which send the order details to the backend, if you try this right now the store won’t work as the saving functionality not complete in the server, we will complete it below. After the order is saved we clear the cart and redirect the user into another page /checkout-thanks.

pages/checkout-thanks

<template>
  <section id="cart_items">
    <div class="container">
      <div class="alert alert-success">
        Order added successfully and will be delivered to the specified shipping address
      </div>
    </div>
  </section>
</template>

<script>
    export default {
        name: "checkout-thanks",
        mounted() {
          setTimeout(() => {
              this.$router.push("/orders");
          }, 3000);
        }
    }
</script>

Also create pages/checkout-paid

<template>
  <section id="cart_items">
    <div class="container">
      <div class="alert alert-success">
        Order paid successfully and will be delivered to the specified shipping address
      </div>
    </div>
  </section>
</template>

<script>
  export default {
    name: "checkout-paid",
    mounted() {
      setTimeout(() => {
        this.$router.push("/orders");
      }, 3000);
    }
  }
</script>

And create pages/checkout-cancel

<template>
  <section id="cart_items">
    <div class="container">
      <div class="alert alert-danger">
        Order cancelled
      </div>
    </div>
  </section>
</template>

<script>
    export default {
        name: "checkout-cancel",
        mounted() {
          setTimeout(() => {
            this.$router.push("/orders");
          }, 3000);
        }
    }
</script>

Before going back to save the order into server let’s complete the paypal code, so open components/checkout-components/Paypal.vue and update it with this code:

<template>
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <i class="fa fa-spinner fa-spin" style="font-size: 33px; margin-left: 46%" v-if="show_loading"></i>
      <div id="paypal-button-container"></div>
    </div>
  </div>
</template>

<script>
    import {OrdersApi} from "../../api/order";

    export default {
        name: "Paypal",
        props: ["cart", "shippingAddress", "paymentMethod", "shippingAddresses"],
        data() {
          return {
            show_loading: false
          }
        },
        methods: {
          getItems() {

            const items = [];
            let total = 0;

            this.cart.map(item => {
              total += item.product.price_after_discount_numeric * item.amount;

              items.push({
                name: item.product.title,
                unit_amount: {
                  value: item.product.price_after_discount_numeric,
                  currency_code: "USD"
                },
                quantity: item.amount
              });
            });

            return {items, total};
          },
          saveOrder(data, success = true) {
            data.shipping_address_id = this.shippingAddress;
            data.payment_method = this.paymentMethod;

            OrdersApi.store(this.$axios, data).then(response => {

              this.$store.commit('cart/clear');

              if(success) {
                this.$router.push("/checkout-paid");
              } else {
                this.$router.push("/checkout-cancel");
              }
            });
          }
        },
        mounted() {
          const {items, total} = this.getItems();

          const shippingAddress = this.shippingAddresses.find(address => address.id == this.shippingAddress);

          setTimeout(() => {
            paypal.Buttons({
              createOrder: (data, actions) => {
                return actions.order.create({
                  prefer: 'return=representation',
                  purchase_units: [{
                    description: 'Purchasing items from online shop e-commerce app',
                    amount: {
                      value: total,
                      currency_code: 'USD',
                      breakdown: {
                        item_total: {
                          currency_code: 'USD',
                          value: total
                        }
                      }
                    },
                    items: items,
                    shipping: {
                      address: {
                        address_line_1: shippingAddress.address,
                        country_code: shippingAddress.country,
                        postal_code: shippingAddress.postal_code,
                        admin_area_2: shippingAddress.city
                      }
                    }
                  }]
                });
              },
              onApprove: (data, actions) => {
                this.show_loading = true;
                return actions.order.capture().then((details) => {
                  this.show_loading = false;
                  alert('Transaction completed by ' + details.payer.name.given_name);
                  this.saveOrder({
                      status: 'success',
                      status_message: 'Order from paypal completed',
                      paypal_order_id: details.id,
                      paypal_email: details.payer.email_address,
                      paypal_given_name: details.payer.name.given_name,
                      paypal_payer_id: details.payer.payer_id
                  });
                });
              },
              onCancel: (data) => {
                alert("Payment cancelled");
                this.saveOrder({
                  status: 'cancelled',
                  status_message: 'Order from paypal cancelled',
                  paypal_order_id: data.orderID
                }, false);
              },
              onError: (err) => {
                alert(err);
              }
            }).render('#paypal-button-container');
          }, 1000);
        }
    }
</script>

In the code above i sent some props to the <Paypal /> component like the cart, selected shipping address, select payment method and shipping addresses. Then i updated the paypal.Buttons() method. Using paypal.Buttons.createOrder(data, actions) i created the order by calling actions.order.create() and passing a list of attributes.

The prefer attribute specify the return type, the purchase_units attribute is any array of objects that specify the items details which represents the total price of the order and the currency and array of items obtained from the helper method getItems() and the shipping attribute which specifies shipping data.

Next the onApprove(data, actions) callback is a good place to handle payment approval, and retrieving the order details and save the order into the database so i executed actions.order.capture() promise and then saved the order using the helper method saveOrder(). The saveOrder() then redirect the user to either /checkout-paid or /checkout-cancel.

The onCancel() and onError() works in the same way as onApprove(), in the onCancel() i saved the order into database with state cancelled, in the onError() i just show an alert message.

Now proceed to the next part to save and manage orders.

 

Continue to Part20: Orders Management

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments