DEV Community

Cover image for Build a Point-of Sale App with Serialized

Build a Point-of Sale App with Serialized

This tutorial was originally written for the Serialized platform. You can view the original post on Serialized's blog here.

When we think of technology, we often donā€™t think of day-to-day businesses like restaurants, kiosks, and shops. However, technology is being used in retail and food service every day! The main technological intersection between these types of businesses is a POS (which stands for ā€œpoint-of-saleā€) system. Itā€™s this program that makes sure you get those tacos you were craving from your favorite restaurant, that sweater youā€™ve been eyeballing on Poshmark, and that new iPhone on the Apple website. They allow employees to ring up and itemize orders too, providing the primary means of communication for orders across the whole business.

Since POS systems are the backbone of many retail and food businesses, I was intrigued by the idea of building one. In this article, weā€™ll dive into building a POS web application that uses React, Express, and Serialized.

What Weā€™re Building

Our POS system will use React for the frontend, Express for the backend, and Serialized to create and store orders, as well as continuously add items to orders.

Serialized is a cloud-hosted API engine for building event-driven systems ā€” it helps us easily capture the comprehensive timeline and history of events and aggregate them into related groups. In relation to our POS system, weā€™ll be using Serialized to keep track of events (customers ordering items) and aggregate them into related groups (customersā€™ orders).

Below is a diagram of what the user flow will look like for the application:

User flow diagram

The three main functionalities weā€™ll focus on in this tutorial are:

  1. creating new orders,
  2. adding items to existing orders, and
  3. marking orders as completed.

These three scenarios will capture the use cases our very basic POS system. The final product will look like this:

Final product GIF of POS system

Getting Started

Before we get started building, make sure you set up the following:

  • Node: To check if you have Node installed already, you can run node -v in your command line. If no version pops up, youā€™ll need to install it - you can find installation directions for your machine here.
  • npx: npx is a package runner for Node packages that allows you to execute packages from the npm registry without needing to install it. To check if you have it installed (typically comes with npm, which comes with Node), you can run npx -v. If no version pops up, you can install npx using the instructions here.
  • Serialized: To use the Serialized API, youā€™ll need to create an account. Once you create an account, itā€™ll prompt you to also create a project, which is also required to start building with the API. You can name your project whatever youā€™d like ā€” I went with POS App. You can learn more about projects in Serialized here.

If youā€™d prefer to walk through code rather than build, Iā€™ve got you! You can view the GitHub repository for this project here. All instructions for running the project are available in the repositoryā€™s README.md in the root directory. (Tip: the GitHub repo is also a great source of guidance if you get stuck while building alongside the tutorial!)

Project Setup

The setup for the project is based on this tutorial from freeCodeCamp.

  1. To start, initialize the project directory on your machine in your location of choice by running mkdir pos-app or creating a pos-app folder manually. cd into it in you Terminal and run

     npx create-react-app client
    

    This will create a folder named client where your applicationā€™s frontend will live.

  2. Once the client folder has been created, run the following commands to enter the newly created client folder, and then start the frontend server:

    cd client
    npm start
    

    If your project has been set up correctly, you should see the default React app in your browser at [localhost:3000](http://localhost:3000):

    React app starter page

  3. If your frontend launched successfully, itā€™s time to now set up the backend! Terminate the frontend server by running CTRL + C. Then, use the command cd ../ from the client folder to switch back into your projectā€™s root directory. Then, run the following commands to generate an Express application in a folder called api and start up the backend:

    npx express-generator api
    cd api
    npm install
    npm start
    

    If your backend was set up correctly, you should see this view after running npm start:

    Express starter page

    You can learn more about the express-generator package used to set up the backend here.

  4. At this point, both the frontend and backend are wired up to localhost:3000. Since youā€™ll need to run both servers at the same time while developing the app, youā€™ll need to change the port the backend runs on to avoid a port collision. To do this, navigate to the bin/www file in the api directory. Update line 15 so its default value now points to port 9000. The line will look like this once updated:

    var port = normalizePort(process.env.PORT || '9000');
    

    Now, when running npm start in the api folder to start up the backend, youā€™ll be able to see the launched Express server at localhost:9000.

Setting up Serialized

  1. In order to use Serialized with the application that was set up in the steps above, you can install the Serialized client for Javascript and Typescript. Since the Serialized API will be called in the Express backend, run the following command to install the client in your api directory:

    npm install @serialized/serialized-client
    
  2. Once the client has been installed, create a .env file in the api directory to set up environment variables for the Serialized API Keys that will be passed into the client to access your account information. Your .env file will contain these two environment variables:

    SERIALIZED_ACCESS_KEY=
    SERIALIZED_SECRET_ACCESS_KEY=
    

    To find the SERIALIZED_ACCESS_KEY and SERIALIZED_SECRET_ACCESS_KEY values, go to Settings > API Keys in your Serialized dashboard for the project you created and set the environment variables to the corresponding values.

Create New Orders

Now that the Serialized API and authorization has been configured, you can make your first call from your application to the API! In this section youā€™ll focus on our first use case of the Serialized Aggregates API to create a new order in our POS system.

  1. To get started, create an order.js file within the api directory. This file will be the scaffolding for defining the concept of an ā€œorderā€ to Serialized. Itā€™s also where you will create or add items to orders, as well as other logic and event handlers for triggering our applicationā€™s functionality.

    Paste the following code into the order.js file:

    const { DomainEvent } = require("@serialized/serialized-client");
    
    class Order {
        get aggregateType() {
          return "order";
        }
    
        constructor(state) {
          this.orderId = state.orderId;
          this.items = state.items;
          this.total = state.total;
          this.completed = state.completed;
        }
    
        createOrder(orderId) {
          if (!orderId || orderId.length !== 36) throw "Invalid orderId";
          return [DomainEvent.create(new OrderCreated(orderId))];
        }
    
        get eventHandlers() {
          return {
            OrderCreated(state, event) {
              console.log("Handling OrderCreated", event);
              return OrderState.newState(event.orderId).withOrderId(event.orderId);
            },
          };
        }
    }
    
    class OrderCreated {
      constructor(orderId) {
        this.orderId = orderId;
      }
    }
    
    class OrderState {
        constructor({ orderId, items = [], total = 0.0, completed = false }) {
          this.orderId = orderId;
          this.items = items;
          this.total = total;
          this.completed = completed;
        }
    
        static newState(orderId) {
          return new OrderState({ orderId });
        }
    
        withOrderId(orderId) {
          return Object.assign({}, this, { orderId });
        }
    }
    
    module.exports = { Order };
    

    To walk through this file, letā€™s break it down class by class:

  • Order: This class is a representation of an actual order object. The Order object is defined as an Aggregate in Serialized, meaning that it is a process that consists of Events, which will be actions that happen to a particular Order object. In this tutorial, these events would be creating new orders, adding an item to an order, and completing the order.

    • As indicated in the Order classā€™s constructor, declaring a new Order instance will require a state object representing the order and its current stats to be passed in. This is because each Aggregate is made up of Events, and theyā€™re responsible for updating the state of the whole order as they get triggered.
    • Next, a createOrder() function is initialized ā€” this will check if a given orderId exists and matches the 36-character UUID format specified for order IDs. Then itā€™ll initialize our new order-creation event with a call to DomainEvent.create().
    • Finally, an eventHandlers() function is declared, which takes in an Orderā€™s current state and the event that happened to the order.
      • At this point in the tutorial, only an OrderCreated event handler has been returned for now, but there will be additional ones added for the other event types. Event handlers will log an event in the console and use the OrderState object to keep track of the Orderā€™s state.
  • OrderCreated: This class represents an event type ā€” in this scenario, itā€™s that a new order was created. Every new event added will require a new class that determines what information the event passes to the API. The class name should match the event handler it corresponds to (in this case, OrderCreated. To create a new order, the only property required is an orderId, so that is the only property declared in this class.

  • OrderState: This class defines an orderā€™s current state and keeps track of it as it changes so it can be passed in as events to the Order object, which will send the events to Serialize as they are triggered. Remember that a change in state could be anything from adding new items to the order to marking it as completed ā€” the latter of which is denoted by the OrderStateā€™s completed property being set to true.

  1. Once your order.js file is set up, add in an order-client.js file in the same directory. This file will act as a client that wires up authentication for the Serialized Aggregates API with the functionality written in order.js. Paste in the following code to the order-client.js file:

    const { Order } = require("./order");
    
    const handleError = async function (handler) {
      try {
        await handler();
      } catch (error) {
        throw new Error("Failed to process command: " + error);
      }
    };
    
    class OrderClient {
      constructor(serializedClient) {
        this.client = serializedClient.aggregateClient(Order);
      }
    
      async createOrder(orderId) {
        await handleError(
          async () =>
            await this.client.create(orderId, (order) => {
              return order.createOrder(orderId);
            })
        );
      }
    }
    
    module.exports = OrderClient;
    

    The file imports the Order class from the previous order.js file. Then, an error handler is initialized to handle generic API request logic of calling a particular function and catching and surfacing any potential errors. Additionally, an OrderClient class is declared. This class assumes an authenticated instance of Serializedā€™s general authentication API client is being passed in (serializedClient), and it uses this to specifically initialize an instance of the clientā€™s Aggregates API client using the aggregateClient() function.

  2. Once order.js and order-client.js have been set up, you can create a route that will initialize an authenticated Serialized API client and make the needed API requests callable from the frontend. Go to the api/routes directory and create a file called orders.js with the following code inside:

    var express = require("express");
    require("dotenv").config();
    var router = express.Router();
    const { Serialized } = require("@serialized/serialized-client");
    const OrderClient = require("../order-client");
    
    const serializedClient = Serialized.create({
      accessKey: process.env.SERIALIZED_ACCESS_KEY,
      secretAccessKey: process.env.SERIALIZED_SECRET_ACCESS_KEY,
    });
    const orderClient = new OrderClient(serializedClient);
    
    router.post("/create", async function (req, res, next) {
      const { orderId } = req.body;
      console.dir(req.body);
      try {
        var response = await orderClient.createOrder(orderId);
        res.send(response);
      } catch (error) {
        console.log(error);
        res.status(400).json({ error: error });
      }
    });
    
    module.exports = router;
    

    The above code initializes an authenticated instance of the Serialized client using your accountā€™s access keys, creates a new instance of the OrderClient defined in order-client.js using this Serialized client, and then calls a function on that OrderClient instance to create a new order based on the information that was passed in. Then, a /create POST route is declared. This route that takes in orderId in the request body. Using the OrderClient instance declared at the top of the file, it then calls the createOrder() function from the order-client.js file and passes in the orderId.

  3. Now that the orders.js route has been created, it needs to be added to the app.js in the api directory so it can be called within the app. Add an initialization for an ordersRouter variable on line 9 in api/app.js:

    var ordersRouter = require("./routes/orders");
    

    Then, in line 24 of api/app.js, add in an app.use() declaration for the ordersRouter to point an /orders route to the endpoints in that file:

    app.use("/orders", ordersRouter);
    

    Now that this route has been added in, we can POST to the /orders/create endpoint on localhost:9000, to create a new order!

Wiring up our React Frontend

Now that the API routes have been configured on the Express side, letā€™s call it from the React frontend! We can set up the frontend application to make an API call to the newly created /orders/create route so we can make an order from the frontend.

  1. Browsers often enforce a same-origin policy for requests, resulting in CORS (Cross-Origin Resource Policy) errors in the event that requests on a certain domain are made from a different origin domain. This example uses [localhost:3000](http://localhost:3000) for the frontend while retrieving information from a [localhost:9000](http://localhost:9000) endpoint from our Express backend ā€” this difference in URLs will potentially create a CORS error, since the browser could say that violates the same-origin policy. To prevent CORS errors in your app once the frontend and backend are wired up, install the CORS package in api with the following command:

    npm install --save cors
    
  2. In api/app.js, add the following on line 6 to add in the CORS package that was just installed to the backend:

    var cors = require("cors");
    

    Then at line 23, add the following line to instruct your Express app to use the CORS package:

    app.use(cors());
    

    It might be worth checking api/app.js against the GitHub repo at this point, just to make sure everything is set up right.

  3. In the client directory, create a new folder inside src called components and initialize a file called POSHome.js:

    import React from "react";
    
    export default function POSHome() {
    
      async function createOrder() {
        var generatedOrderId = crypto.randomUUID();
        var data = { orderId: generatedOrderId };
        var order = await fetch("http://localhost:9000/orders/create", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(data),
        });
      }
    
      return (
        <div>
          <h1>POS System ā˜•ļø</h1>
          <div>
            <button onClick={createOrder}>Create Order</button>
          </div>
        </div>
      );
    }
    

    This file declares a functional component called POSHome (which is where the homepage of the POS system will live).

    On this page, there will be a button that, when clicked, calls createOrder(). That function uses crypto.randomUUID() to generate a UUID that will fit the standards the backend is expecting, shoves it all into the data object, and sends it off to our new /orders/create endpoint.

  4. Replace client/src/App.js with the following code so that the POSHome component is being passed in to the main application and is visible from the main page:

    import "./App.css";
    import POSHome from "./components/POSHome";
    
    function App() {
      return (
        <div className="App">
            <POSHome />
        </div>
      );
    }
    
    export default App;
    
  5. Open a new window or tab in the Terminal so that you have two tabs or windows open. In one tab, run npm start in the api folder. In another tab, run npm start in the client folder. Once [localhost:3000](http://localhost:3000) launches the frontend, youā€™ll see the following screen:

    POS System screenshot

    Click the Create Order button and then go to your Serialized dashboard for your project and go to the Data Explorer page. You should see an entry for a new order ā€” the one we just created on page load from the POSHome frontend component calling the /orders/create endpoint:

    Screenshot of Serialized dashboard with new order entry

    If you check the Terminal tab or window where youā€™re running the api server, youā€™ll also see something like the following:

    OPTIONS /orders/create 204 0.236 ms - 0
    { orderId: 'd3ce8600-9e71-4417-9726-ab3b9056df48' }
    POST /orders/create 200 719.752 ms - -
    

    This is an event log from the backend endpoint recording the instance of the new order being created. Any console.log statements made from the backend will also show up here.

Integrating our functionality into our application

Now that youā€™ve taken a dive into the frontend code, letā€™s lay out the remaining flow for creating, adding items, and then completing an order.

  1. Letā€™s start by initializing a dataset that will represent the items youā€™ll be selling in your POS. In client/src, create a folder called data and add in an items.json file. Within the file, set up something like this:

    {
        "items": [
            {
                "name": "Tea",
                "price": 3.99
            },
            {
                "name": "Coffee",
                "price": 4.99
            },
            {
                "name": "Bagel",
                "price": 2.50
            }
        ]
    }
    

    Here we added some inventory items to the items property array, each with a name and price property.

  2. Now that data has been added for what items are sold in the POS system, it needs to be surfaced in a view. This will require a new component that is shown only when the Create Order button added in the last step is clicked. In client/src/components, add an ItemDisplay.js file for a new checkout flow component. Hereā€™s what that might look like:

    import React from "react";
    
    export default function ItemDisplay (props) {
      var data = require("../data/items.json");
    
      return (
        <div>
          <div>
            {data.items.map((item, index) => {
              return (
                <button key={index}>
                  {item.name}
                </button>
              );
            })}
          </div>
        </div>
      );
    }
    

    Within the ItemDisplay component, the data from items.json is imported into the data variable. Then, in the return of the component, each item in data is iterated through and replaced with a button carrying that itemā€™s name as a label.

  3. Now, letā€™s update client/src/components/POSHome.js so that when an order is created, itā€™ll display the ItemDisplay component. Weā€™ll use state variables for that ā€” itā€™s great for conditionally rendering components. To start, update the import line at the top of POSHome.js so it imports the useState hook too. While weā€™re there, bring in the ItemDisplay component from earlier.

    import React, { useState } from "react";
    import ItemDisplay from "./ItemDisplay";
    
  4. The useState hook will initialize a state variable for us and give us a way to update it in the future. Letā€™s start with startedOrder ā€” this will keep track of whether an order has been started, and if so, it will display the ItemDisplay component. The variable will be initialized on line 5 with an initial value of false using the following:

    const [startedOrder, setStartedOrder] = useState(false);
    
  5. Next, update your return() function in your POSHome component so that it looks like the following:

    return (
      <div>
        <h1>POS System ā˜•ļø</h1>
        {!startedOrder && (
          <div>
            <button onClick={createOrder}>Create Order</button>
          </div>
        )}
        {startedOrder && (
          <ItemDisplay />
        )}
      </div>
    );
    

    In the above, JSX is being used to conditionally render certain elements depending on the value of the startedOrder state variable. The logic implement here says: ā€œIf itā€™s false, render the Create Order button. If itā€™s true, render the ItemDisplay component.ā€

  6. The final piece of this is setting startedOrder to true when an order is created. This can be done in the createOrder() function above. Add the following block inside the function on line 15:

    // if order was successful
    if (order.status === 200) {
      setStartedOrder(true);
      setOrderId(generatedOrderId);
    }
    
  7. Now itā€™s time to test the flow! Load up the frontend and backend of your application by running npm start in both the api and client directories in two different Terminal tabs or windows. Once the client has loaded, you should see your application appear in localhost:3000. Click the Create Order button and you should see your items appear as buttons on the page like in the screenshot below. This page, showing the ItemDisplay component, is where youā€™ll be able to select your items and add them to your order, which will be added in the section below.

POS system order page with buttons for items

Adding Items to Orders

Now weā€™re showing the available items, we need to be able to add those items to the running order.

To get started, letā€™s first jump into the backend.

  1. In /client/api/order.js, add in an ItemAdded event class under where the OrderCreated class is declared:

    class ItemAdded {
      constructor(orderId, itemName, itemPrice) {
        this.orderId = orderId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
      }
    }
    

    This declares a class for a new event, ItemAdded, that will take in an orderId, itemName, and itemPrice.

  2. Add an itemAdded() function to your Order class by adding the following code at line 19:

    addItem(itemName, itemPrice) {
      if (this.completed)
        throw "List cannot be changed since it has been completed";
      return [DomainEvent.create(new ItemAdded(this.orderId, itemName, itemPrice))];
    }
    

    This function will first check if an order is completed - if it is, itā€™ll throw an error, as new items cannot be added. If it isnā€™t, itā€™ll pull the orderId directly from the Order object instance and take in an itemName and itemPrice to log an event instance of what item was added in to the order.

  3. In the Order class, add a new event handler for an item to be added:

    ItemAdded(state, event) {
      console.log("Handling ItemAdded", event);
      return new Order(state).addItem({
        orderId: event.orderId,
        itemName: event.itemName,
        itemPrice: event.itemPrice
      });
    },
    
  4. Add the following inside the OrderState class at line 64:

    addItem(itemName, itemPrice) {
      return Object.assign({}, this, { items: this.items.unshift({itemName: itemName, itemPrice: itemPrice}) });
    }
    

    The above code will update the items array property of the OrderState object so that the new item is pushed onto the array.

    At this point, itā€™s probably a good idea to match your order.js against the GitHub repo to make sure it lines up.

  5. Once api/order.js has been updated, jump into the order-client.js file to add an addItem() function that will query the addItem() logic that was just added. Paste the following inside the OrderClient class at line 24:

    async addItem(orderId, itemName) {
      await handleError(
        async () =>
          await this.client.update(orderId, (order) => {
            return order.addItem(itemName);
          })
      );
    }
    
  6. Finally, add a route in api/routes/orders.js so that the functionality to add an item to an order can be called from the frontend. Add this code on line 24:

    router.post("/add-item", async function (req, res, next) {
      const { orderId, itemName, itemPrice } = req.body;
      console.dir(req.body);
      try {
        var response = await orderClient.addItem(orderId, itemName, itemPrice);
        res.send(response);
      } catch (error) {
        console.log(error);
        res.status(400).json({ error: error });
      }
    });
    

    The above request will create an endpoint at /orders/add-item that takes in an orderId, itemName, and itemPrice in its request body to add an item and take note of its properties when itā€™s added to an order of a certain orderId.

Consuming the endpoint we just made

Now that the backend is complete, letā€™s call this endpoint in the frontend! When an item button is selected in the ItemDisplay component, it should trigger the /orders/add-item endpoint and also display an itemized receipt and total order amount of items added so far in the order.

  1. To start, go to /client/src/POSHome.js. Since the /add-item request takes in an orderId, we need to pass it in to the ItemDisplay component to make the API call. To do so, youā€™ll need a state variable to keep track of order IDs. Add the following state variabl declaration:

    const [orderId, setOrderId] = useState("");
    
  2. Then, within createOrder(), add the following line under setStartedOrder(true); to set the orderId state variable to the order ID of a successfully created (and therefore current) order:

    setOrderId(generatedOrderId);
    
  3. Finally update the <ItemDisplay /> line in your return() to the following to pass the orderId state variable in as a prop:

    <ItemDisplay orderId={orderId} />
    
  4. Perfect! To keep track of our selected items, letā€™s do something similar in /client/src/ItemDisplay.js. In there, import the useState hook at the top just like we did with POSHome and initialize the itemsInOrder and orderTotal state variables like this:

    const [itemsInOrder, setItemsInOrder] = useState([]);
    const [orderTotal, setOrderTotal] = useState(0);
    
  5. Once the state variables have been added in, letā€™s add in a function called addItemToOrder() that will call the /orders/add-item endpoint we made earlier. Add the following function to the ItemDisplay component above the return():

    async function addItemToOrder (name, price) {
      // add in item to order
      var data = { orderId: props.orderId, itemName: name, itemPrice: roundedPrice };
      var order = await fetch("http://localhost:9000/orders/add-item", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
    
      // if order was successful
      if (order.status === 200) {
        var roundedPrice = price.toFixed(2);
        // push item name to setItemsInOrder
        // add total to orderTotal
        setItemsInOrder([...itemsInOrder, { name: name, price: roundedPrice }]);
        setOrderTotal(orderTotal + price);
      }
    }
    

    The function will take in an itemā€™s name and price. Then, the data object is declared that takes in orderId, itemName, and itemPrice, the requirements for the requestā€™s body. Finally, the request is made with all the necessary data passed in. If the order ends up being successful, in order to display a price that has two decimal places, the price is converted using price.toFixed(2). Then, the itemā€™s name and price are added to the itemsInOrder array, while the itemā€™s price is added to the orderā€™s total.

  6. Add an onClick event to the <button> tag in the return(). Within the event, call the addItemToOrder() function. The tag should look like this:

    <button
      key={index}
      onClick={() => {
        addItemToOrder(item.name, item.price);
      }}
    >
    

    This will fire the addItemToOrder() function each time an itemā€™s button is clicked.

  7. Within the main <div> in the return() function, after the first nested <div>, add a section to show an itemā€™s name and price, as well as the order total. It will dynamically update as the ordreTotal and itemsInOrder state variables are updated.

    <div>
      <h2>Items Ordered</h2>
      <ul className="receipt">
        {itemsInOrder.map((item, index) => {
          return (
            <li key={index}>
              <div className="receiptEntry">
                <div className="itemName">{item.name}</div>
                <div className="itemPrice">{"$" + item.price}</div>
              </div>
            </li>
          );
        })}
      </ul>
      <p>
        <b>Order Total:</b> ${(Math.round(orderTotal * 100) / 100).toFixed(2)}
      </p>
    </div>
    
  8. Finally, itā€™s time to test the functionality! Start up the frontend and backend of your application. Once the application loads, click the Create Order button. You should see the following page:

    POS order page with items listed and items ordered section

    As you click on the buttons, the item name and price should appear under ā€œItems Orderedā€, and the order total should also increase. Hereā€™s an example of what it should look like if you click ā€œTeaā€, ā€œCoffeeā€, and ā€œBagelā€:

    POS system with list of items selected

    To confirm items have been added to an order, go to your Serialized Dashboard > Data explorer > Aggregates > order (under Aggregate type column) > Aggregates > click the Aggregate ID of the top (and most recent) entry. You should then see a view like this:

    Aggregate view of Serialized

    If you click into any of the ItemAdded Event IDs, youā€™ll see an object containing the data sent from the ItemAdded event in your app:

    Item added event IDs page in Serialized

    The above ItemAdded event was for a $2.50 bagel that was added to the order.

Completing Orders

The final use case will be completing orders. Once an order is completed from the ItemDisplay component, the component will disappear and the Create Order button will appear again to start a new order.

Letā€™s start in the backend!

  1. First, in /client/api/order.js, add in an OrderCompleted event class:

    class OrderCompleted {
      constructor(orderId, total) {
        this.orderId = orderId;
        this.total = total;
      }
    }
    

    This event class requires an orderId and a final order total to complete the order.

  2. Similar to the addOrder flow, weā€™ll need to add a new completeOrder() function to the Order class:

    completeOrder(total) {
      if (!this.completed) {
        return [DomainEvent.create(new OrderCompleted(this.orderId, total))];
      } else {
        // Don't emit event if already completed
        return [];
      }
    }
    

    The above function will first check if an order is completed or not. If it isnā€™t completed, then a new event will be created of the OrderCompleted class type that was added above. It also passes in the necessary properties, taking the orderId from the Order object instance and passing in the total.

  3. Next, add an OrderCompleted event handler:

    OrderCompleted(state, event) {
      console.log("Handling OrderCompleted", event);
      return new Order(state).completeOrder({
        orderId: event.orderId,
        total: event.total,
      });
    },
    
  4. Then, in OrderState, add a completeOrder function:

    completeOrder(total) {
      return Object.assign({}, this, { completed: true, total: total });
    }
    
  5. Next, in api/order-client.js, add in a function, completeOrder(), to call completeOrder() from order.js:

    async completeOrder(orderId, total) {
      await handleError(
        async () =>
          await this.client.update(orderId, (order) => {
            return order.completeOrder(total);
          })
      );
    }
    
  6. Finally, add in a /orders/complete route to api/routes/orders.js:

    router.post("/complete", async function (req, res, next) {
      const { orderId, total } = req.body;
      console.dir(req.body);
      try {
        var response = await orderClient.completeOrder(orderId, total);
        res.send(response);
      } catch (error) {
        console.log(error);
        res.status(400).json({ error: error });
      }
    });
    

Letā€™s jump back to the frontend for a bit.

  1. In order for this logic to work from ItemDisplay, youā€™ll need to update the startedOrder state variable from the ItemDisplay component. To do this, the setStartedOrder function can be passed in as a property from POSHome. In client/src/components/POSHome.js, pass in setStartedOrder to the <ItemDisplay> component so that it looks like this:

    <ItemDisplay orderId={orderId} setStartedOrder={setStartedOrder} />
    
  2. Now, in /client/src/components/ItemDisplay.js, add a new function, completeOrder(). This will make a call to the /orders/complete endpoint and pass in an orderId variable from props as well as the orderTotal state variable.

    async function completeOrder() {
      // add in item to order
      var data = { orderId: props.orderId, total: orderTotal };
      var order = await fetch("http://localhost:9000/orders/complete", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
    
      // if order was successful
      if (order.status === 200) {
        props.setStartedOrder(false);
      }
    }
    
    function exitOrder() {
      props.setStartedOrder(false);
    }
    

    These two functions are the choices that a user can take when theyā€™re on this screen. They can complete the order ā€” in which case the setStartedOrder() function will be called and the state variable will be set to false, triggering that conditional statement we made earlier ā€” or they can just exit everything. Link these up to buttons in our render function so the user can call this code. Itā€™s all coming together!

  3. Now itā€™s time to test your application! Run the frontend and backend in two different Terminal windows and test the end-to-end flow. It should look like this:

    GIF of full POS order flow

  4. To confirm orders were marked as completed, go to your Serialized Dashboard and navigate to Data explorer ā†’ Aggregates ā†’ order (under Aggregate type column) ā†’ Aggregates. Click the Aggregate ID of the top (and most recent) entry. You should then see a view like this:

    Serialized aggregate event list

    If you click on the Event ID for the OrderCompleted event, it will surface data sent from the app (the orderā€™s total amount):

    Serialized event data dump with order amount

Looking back

At this point, the only thing missing is a little CSS. This tutorial is already a bit long, so Iā€™ll leave that as an exercise for the reader, but if youā€™d like, you can always check out what I wrote in the GitHub repo. This is what it ended up looking like:

Completed POS app with CSS

Iā€™m really satisfied with what weā€™ve created! We managed to use Serializedā€™s Aggregates API to create a very simple POS (point-of-sale) application so users can create orders, add items to an order, and either complete or exit the order. All events that occur within this order are sent to Serialized, where they are stored in groups of events, or Aggregates, with each Aggregate instance representing an order.

We might come back to this in the future to show off the other half of Serializedā€™s functionality that we havenā€™t even gotten to touch, but if youā€™re looking to build more on top of this application yourself, perhaps try to:

  • Experiment with making the UI more sophisticated - adding pictures for items, adding more items, even adding item descriptions and sending these to Serialized!
  • Add frontend and backend testing for the components, functionality, requests, and routes.

Thanks so much for following along! You can connect with me on Twitter and feel free to reach out if there are any questions or feedback. ā­ļø

Top comments (0)