Building an Order Management System
With Firebase, Quickbooks, Stripe, and ShipEngine.
I built an OMS for Ready Planner because I wanted to avoid lock-in with services like Shopify and to have full control over my store. My needs were simple:
- Use Stripe for payment processing.
- Manage physical inventory and drop-ship items.
- Support sales and promotions.
- Connect with Quickbooks to keep bookkeeping simple.
- Get shipping estimates.
- Send automatic receipt emails.
My solution is far simpler than what comes with a packaged service. My main requirements are for the customer to be able to accurately place orders and for me to be able to track the orders in Quickbooks.
I'm using Firebase for the back-end but this solution will most likely work for another serverless architecture. One of the features with Firebase that I use heavily is the ability to register a cloud function that listens to database updates. If there are other serverless solutions that offer that ability then let me know in the comments at the bottom of this post.
Front-end
I wanted the front-end to be fast so I opted to use a Multi-page Application (MPA) of static pages backed by Firebases CDN. I use NextJS to dynamically create the static pages at build time. It uses Redux to handle adding/removing products to the cart and ties the user's cart with the Firebase auth service. The Redux store syncs perfectly with the Firestore database because they are both JSON structures.
Simplified Application flow
The high level application flow works like this:
- Site loads and immediately checks if there is an existing user in the database.
- User adds a product to the cart which is made up of a SKU and a quantity:
{ sku: "some-sku-id", quantity: 1 }
. The cart is immediately updated on the client to provide instant feedback and later makes a request to sync the DB. - The user at some point navigates to the checkout page.
- The checkout page ensures the DB is synced with the local store and if it isn't will show a loading spinner until it is.
- The checkout page will then check if it can place holds on the inventory items in the cart. This helps ensure more than one customer can't claim an inventory item at the same time. If the inventory in the cart is in stock then it will place the holds in the DB.
- If the inventory is not in stock by the time the user gets to the checkout page then the user will be notified. The items will still be held in the cart in-case at a later time the inventory opens up.
- Once holds are placed it creates an Order in the DB tied to the User's ID.
- The user enters their name and email and requests the back-end to check if there are promotions tied to the user's email via an onCall function.
- The user enters their address and an onCall function fires an API request to ShipEngine to verify the address and get shipping rates.
- The UI requests confirmation of the user's address if it differs from the verified address.
- The user clicks the "Continue to Payment" button which shows a loading spinner while I get things ready to redirect to Stripe.
- The user enters their payment details in Stripe Checkout and a payment intent is created. The payment intent will hold the funds from the user's bank but won't charge it until I say so. This allows me to confirm inventory items are in-stock.
- There is a cloud function listening for a webhook from Stripe. When it receives the payment hook it updates the order in the DB which triggers the order update function.
- The order update function fires an API request to Quickbooks to create an invoice with all the order details.
- I periodically check Quickbooks to see if there are new orders. For now this is OK but at some point I'll set up an alerting service to know when new orders come in.
- If the order in Quickbooks looks good then I'll mark the invoice paid which fires a webhook and tells the order in the database that the invoice is ready for payment.
- An API request is made to Stripe to collect payment and Stripe sends the customer a receipt.
- If there are on-demand books that need to be printed and drop-shipped then an API request is made to my printer for fulfillment or I pack a box with physical inventory and ship it (the latter rarely happens as most orders are on-demand).
- The order is complete!
I'll go into more details on how these steps are completed in other posts. If you'd like to know when those are published you can follow the blog here.