24 C
en
  • Home
  • Blog
  • About
MyCovers, covering dynamic ecommerce

Mega Menu

  • NEWS
  • REVIEWS
  • RECOMMENDED PRODUCTS
  • EDUCATIONAL ARTICLES
    • Hydrogen
    • React
    • Tailwind
    • UI/UX
    • Design
  • YOUTUBE
MyCovers, covering dynamic ecommerce
Search
Home Backend Headless eCommerce HotNews Hydrogen TechNews Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)
Backend Headless eCommerce HotNews Hydrogen TechNews

Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

Faiza Tanjia
Faiza Tanjia
11 May, 2022 0 0
Facebook
Twitter
Telegram
WhatsApp
Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

1. Introduction

In episode 1 and 2, we have created a long-living shopping cart built on Temporal with an e-money payment and email notification. We are almost there to a fully functional app. For this last part, we’re going to build a REST API to run the signals that we created before in Workflows.

2. REST-API with express

Since the API written using express is considered a Temporal client (since it interacts with the existing Workflow), it will use classes from @temporalio/client

Connect to the existing workflow.

src/api/main.js

import { Connection, WorkflowClient } from '@temporalio/client'; import { CartWorkflow } from '../workflows.js'; async function run() { const connection = new Connection({ // // Connect to localhost with default ConnectionOptions. // // In production, pass options to the Connection constructor to configure TLS and other settings: // address: 'foo.bar.tmprl.cloud', // as provisioned // tls: {} // as provisioned }); const client = new WorkflowClient(connection.service, { // namespace: 'default', // change if you have a different namespace }); const result = await client.start(CartWorkflow , { taskQueue: 'temporal-ecommerce', // in practice, use a meaningful business id, eg customerId or transactionId workflowId: temporal-ecommerce-business-id-45, args: [], }); } run().catch((err) => { console.error(err); process.exit(1); });

The above code will connect to the CartWorkflow Workflow that we wrote in part 2. An Express web server process can run in this client.

src/api/main.js

import { Connection, WorkflowClient } from '@temporalio/client'; import { CartWorkflow } from '../workflows.js'; import { createExpressMiddleware } from 'temporal-rest'; import express from 'express'; import bodyparser from 'body-parser'; async function run() { const connection = new Connection({ // // Connect to localhost with default ConnectionOptions. // // In production, pass options to the Connection constructor to configure TLS and other settings: // address: 'foo.bar.tmprl.cloud', // as provisioned // tls: {} // as provisioned }); const client = new WorkflowClient(connection.service, { // namespace: 'default', // change if you have a different namespace }); // express const app = express(); app.use(createExpressMiddleware(CartWorkflow, client, 'express-ecommerce')); // notes: body parser needed (if not included, can't read request data) // parse application/x-www-form-urlencoded app.use(bodyparser.urlencoded({ extended: false })); // parse application/json app.use(bodyparser.json()); await app.listen(8393); console.log('listening on port 8393'); const result = await client.start(CartWorkflow , { taskQueue: 'temporal-ecommerce', // in practice, use a meaningful business id, eg customerId or transactionId workflowId: 'temporal-ecommerce-business-id-45', args: [], }); } run().catch((err) => { console.error(err); process.exit(1); });

Now our app is successfully listening on port 8393.

3. Shopping Cart Routes

Since we have express already available in our app. Let’s make it possible to do some interactions with an API. The following are the available routes and its corresponding activity that we are going to write.

  • GET /products

    Receive a list of available products

  • POST /cart

    Create a shopping cart

  • GET /cart/{workflowID}

    Get the current state of the shopping cart

  • PUT /cart/{workflowID}/add

    Add item to cart

  • PUT /cart/{workflowID}/remove

    Remove item from cart

  • PUT /cart/{workflowID}/checkout

    Proceed to checkout

Now that we know all the routes needed, let’s get on to create it.

3.1. Receive a list of available products

For this method, it will return the user a list of available products as the name suggests. But we haven’t had a list of products lying around, we need to create one–a simple JS file that exports a list of product objects.

src/products.js

export const products = [ { id: '0', name: 'iPhone 12 Pro', description: 'Shoot amazing videos and photos with the Ultra Wide, Wide, and Telephoto cameras.', image: 'https://images.unsplash.com/photo-1603921326210-6edd2d60ca68', price: 999, }, { id: '1', name: 'iPhone 12', description: 'The iPhone 12 sports a gorgeous new design, full 5G support, great cameras and even better performance', image: 'https://images.unsplash.com/photo-1611472173362-3f53dbd65d80', price: 699, }, { id: '2', name: 'iPhone SE', description: 'The Most Affordable iPhone Features A13 Bionic, the Fastest Chip in a Smartphone, and the Best Single-Camera System in an iPhone', image: 'https://images.unsplash.com/photo-1529618160092-2f8ccc8e087b', price: 399, }, { id: '3', name: 'iPhone 11', description: 'The iPhone 11 offers superb cameras, fast performance and excellent battery life for an affordable price', image: 'https://images.unsplash.com/photo-1574755393849-623942496936', price: 599, } ];

This products will be imported and saved as a state in Workflow so it can be retrievable from outside.

src/workflows.js

import * as wf from '@temporalio/workflow'; import { products as initialProducts } from './products.js'; // import products const { createMidtransPayment, sendAbandonedCartEmail } = wf.proxyActivities({ startToCloseTimeout: '1 minute', }); // create ProductsState (for querying GET /products) export const ProductsState = useState("ProductsState", initialProducts); // .... export async function CartWorkflow() { // .... } /** Utility state function */ // ...

Now it is accessible for outside of Workflow, import ProductsState on our temporal web server client.

src/api/main.js

import { Connection, WorkflowClient } from '@temporalio/client'; import { CartWorkflow, ProductsState, } from '../workflows.js'; import { createExpressMiddleware } from 'temporal-rest'; import express from 'express'; import bodyparser from 'body-parser'; async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // get products state from workflow const productResponse = await ProductsState.value // sending products (json response) to client res.send({ status: '200 OK', products: productResponse }); }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

What the code above do is to retrieve the exported products state that came from Workflow, get the value of it, and send it as a response.

3.2. Create a shopping cart

For every shopping cart creation for every workflowId will need a name and email (two important arguments when sending an email to a customer). To accommodate tihs need, we would need a function to change the main state to whatever the user inputs.

These handlers and its signals must be written directly and exported from Workflow

Write the signals first

src/workflows.js

import * as wf from '@temporalio/workflow'; import { products as initialProducts } from './products.js'; // import products const { createMidtransPayment, sendAbandonedCartEmail } = wf.proxyActivities({ startToCloseTimeout: '1 minute', }); // create ProductsState (for querying GET /products) export const ProductsState = useState("ProductsState", initialProducts); // two handlers for update email and name data for cartstate export const UpdateEmailSignal = wf.defineSignal('UpdateEmail'); export const UpdateNameSignal = wf.defineSignal('UpdateName'); // .... other signals that we wrote before export async function CartWorkflow() { // .... } /** Utility state function */ // ...

Handler to update the email and name state to a new value

src/workflows.js

// .... /** Cart workflow */ export async function CartWorkflow() { // create CartState const CartState = useState("CartState", {items: [], status: 'IN_PROGRESS', email: '', name: ''}); // Update Email Handler wf.setHandler(UpdateEmailSignal, function updateEmailSignal(email) { // set cartState.email to user inputted email CartState.value.email = email; console.log('Update Email Handler finished, current email: ', email); }) // Update Name Handler wf.setHandler(UpdateNameSignal, function updateNameSignal(name) { // set cartState.name to user inputted name CartState.value.name = name; console.log('Update Name Handler finished, current name: ', name); }) // .... } /** Utility state function */ // ....

So when the POST /cart route is called, it will modify the current cart wate with the request body property value of email and name. It will return a respond containing the current cart and current workflow ID.

Import update email and name signals and write the method

src/api/main.js

import { Connection, WorkflowClient } from '@temporalio/client'; import { CartWorkflow, ProductsState, UpdateEmailSignal, UpdateNameSignal, } from '../workflows.js'; import { createExpressMiddleware } from 'temporal-rest'; import express from 'express'; import bodyparser from 'body-parser'; async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // .... }); /** create cart */ app.post('/cart', async (req, res) => { // would need to fetch current state of cart (with getHandle) const handle = await client.getHandle('temporal-ecommerce-business-id-45'); await handle.signal(UpdateEmailSignal, req.body.email); // add emailto state await handle.signal(UpdateNameSignal, req.body.name); // add name to state // fetch new updated cartState const currentCart = await handle.query("CartState") // sending cart response (cart array and workflow ID) to client res.send({ status: '200 OK', body: 'creating cart...', cart: currentCart, workflowID: handle.workflowId }); }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

3.3. Get the current state of the shopping cart

Getting all the property of the cart state is the simples of all methods, all we need to do is fetch the current state and return it as it is, with the request params property of workflow ID as its argument when getting the handle. No need to write additional code like before.

src/api/main.js

// .... async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // .... }); /** create cart */ app.post('/cart', async (req, res) => { // .... }); /** get cart (workflowID) */ app.get('/cart/:workflowID', async (req, res) => { // Functionality is almost the same as the above method, but we will be only returning the // value of cart object // get handler const handle = await client.getHandle(req.params.workflowID); // from the /:workflowID const currentCart = await handle.query("CartState"); // send cart state as json res.send(currentCart); }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

3.4. Add item to cart

Because we have declared the signal and handler since part 1, we just need to import its signal at the top and run the signal with the request property value of body. Add item to cart signal accepts a product object with the same pattern as the item in our list of products (in src.products.js).

src/api/main.js

import { CartWorkflow, ProductsState, UpdateEmailSignal, UpdateNameSignal, AddToCartSignal, } from '../workflows.js'; // .... async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // .... }); /** create cart */ app.post('/cart', async (req, res) => { // .... }); /** get cart (workflowID) */ app.get('/cart/:workflowID', async (req, res) => { // .... }); /** add product to cart */ app.put('/cart/:workflowID/add', async (req, res) => { // get our workflow handler const handle = await client.getHandle(req.params.workflowID); const cartState = await handle.query("CartState"); // get our cart state // call our add to cart handler with req.body as product to add await handle.signal(AddToCartSignal, req.body) // send response to client res.send({ status: '200 OK', body: 'Add to cart successful!', ok: req.body.quantity }) }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

3.5. Remove item from cart

Like add item to cart method, this method only accepts a product object with properties of id, name, description, image, and price (id and price is mandatory). It will run RemoveFromCartSignal with req.body as its argument.

src/api/main.js

import { CartWorkflow, ProductsState, UpdateEmailSignal, UpdateNameSignal, AddToCartSignal, RemoveFromCartSignal } from '../workflows.js'; // .... async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // .... }); /** create cart */ app.post('/cart', async (req, res) => { // .... }); /** get cart (workflowID) */ app.get('/cart/:workflowID', async (req, res) => { // .... }); /** add product to cart */ app.put('/cart/:workflowID/add', async (req, res) => { // .... }); /** remove product from cart */ app.put('/cart/:workflowID/remove', async (req, res) => { // get handler const handle = await client.getHandle(req.params.workflowID); // call remove product signal await handle.signal(RemoveFromCartSignal, req.body); res.send({ status: '200 OK', body: 'Removed from cart', id: req.body.id, product: req.body.name, quantity: req.body.quantity, }); }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

3.6. Proceed to checkout

For checkout, beside calling the CheckoutSignal we will also query Transaction ID and QR Code value from Workflow to pass it as a response. There will be a delay for a few seconds inbetween the process to ensure that the queried value is not the initial value.

src/api/main.js

import { CartWorkflow, ProductsState, UpdateEmailSignal, UpdateNameSignal, AddToCartSignal, RemoveFromCartSignal, CheckoutSignal } from '../workflows.js'; // .... async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // .... }); /** create cart */ app.post('/cart', async (req, res) => { // .... }); /** get cart (workflowID) */ app.get('/cart/:workflowID', async (req, res) => { // .... }); /** add product to cart */ app.put('/cart/:workflowID/add', async (req, res) => { // .... }); /** remove product from cart */ app.put('/cart/:workflowID/remove', async (req, res) => { // .... }); /** checkout */ app.put('/cart/:workflowID/checkout', async (req, res) => { const handle = await client.getHandle(req.params.workflowID); // call checkout signal await handle.signal(CheckoutSignal); // wait for a few second const delay = ms => new Promise(res => setTimeout(res, ms)); await delay(5000); console.log('delayed 5 seconds.') // retrieve transaction id and qr code const transactionId = await handle.query("TransactionId"); const qrCode = await handle.query("QRCode"); res.send({ status: '200 OK', body: 'checked out.', workflowID: handle.workflowId, transactionID: transactionId, qr: qrCode }); }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

4. Handling After-Payment Notification

We have all the methods written, but not after-payment handling yet. If you look at the Workflow, the transaction will be successful only if the IsPaid state has a value of true. So how do we change this value to true if we already received the payment from the customer? We will use Webhook Payment Status ( the documentation of this topic can be accessed here: HTTP(S) Notification / Webhooks of Payment Status ).

We would need an exclusive route reserved just for listening to payment transaction notification. Because it listens to notification, the route would be named as it is.

src/api/main.js

// .... async function run() { // .... /** GET products */ app.get('/products', async (req, res) => { // .... }); /** create cart */ app.post('/cart', async (req, res) => { // .... }); /** get cart (workflowID) */ app.get('/cart/:workflowID', async (req, res) => { // .... }); /** add product to cart */ app.put('/cart/:workflowID/add', async (req, res) => { // .... }); /** remove product from cart */ app.put('/cart/:workflowID/remove', async (req, res) => { // .... }); /** checkout */ app.put('/cart/:workflowID/checkout', async (req, res) => { // .... }); // notification listener to listen for payment fulfillment app.post('/notification', async (req, res) => { console.log('POST /notification received.'); console.log(req.body); res.send({ status: '200 OK', body: 'POST /notification successful.' }); /** Notification Handler */ const handle = await client.getHandle('temporal-ecommerce-business-id-45'); // get transaction ID from workflow const transactionId = await handle.query("TransactionId"); /** post-payment handling */ if (req.body.transaction_status === 'settlement' && req.body.transaction_id === await transactionId) { // change transactionStatus to paid await handle.signal(UpdateTransactionStatusSignal, 'PAID'); console.log('changed transaction status -> PAID') console.log('change IsPaid to true'); // change IsPaid to true await handle.signal("IsPaid", true); } }); await app.listen(8393); console.log('listening on port 8393'); // .... } run().catch((err) => { console.error(err); process.exit(1); });

The code above listens to every transaction status change, when its value is equal to ‘SETTLEMENT’, that’s when the transaction is paid by the customer. When that time comes, we would want to change the IsPaid state to true and the Workflow will be finished.

To make our listener endpoint working, we need to expose our local server port to the internet and use that URL for midtrans’ Payment Notification URL (required to set it from the dashboard, I will show you later).

One way to do that is to use ngrok ( Installation Guide ). If you have already installed ngrok, from your project directory, type the following command:

./ngrok http 8393

It will give us an URL that we can set at midtrans dashboard (Settings–Configuration). Put the URL on the Payment Notification URL section like the following.

set-midtrans-notification

After doing all of this, all we need to do is run our app.

5. Running our App + End Notes

Running our App

To run our app, we need to open another two different terminal and run the following command.

In a terminal (and in your project directory), run:

npm run start.watch

This will start the Workflow (if you haven’t run the temporal server beforehand, the Workflow won’t run).

In another terminal,run the following command to start the express web server

cd src node api/main.js

Using Postman to Test our API

a. Get products
postman-get-products
b. Create cart
postman-create-cart
c. Get cart
postman-get-cart
d. Add item to cart
postman-add-to-cart
e. Delete item from cart
postman-delete-item
f. Checkout
postman-checkout

After we checked out, it will create a transaction and return a QR code that you need to pay in order to finish the order.

created-transaction

When a new transaction is created, midtrans will send us a notification that this certain transaction now has a status of pending (waiting for payment fulfillment).

pending-notification

Because we are using a sandbox environment of midtrans, we can’t really pay it with real money, but we can simulate pay using midtrans mock payment .

Paste the QR link generated from the terminal and click the Scan QR button.

mock-payment-midtrans

The transaction will be marked as successful.

transaction-successful

After the transaction is now marked successful, midtrans will send us a notification that the status has changed (settlement) to our /notification route that we wrote before.

settlement-notification

When a transaction has reached to this point, our workflow task will be finished ant it is marked as done.

finished-workflow

But if we leave the workflow as it is without doing the payment and when the timeout expires, it will send an abandoned cart email to the registered email like the proceeding.

abandoned-cart-email sent-abandoned-cart-email

Next Up

After following this post, we can learn that we can build a RESTful API on top of Temporal by making HTTP requests, other than that it simplifies many jobs that usually require a scheduled job queue (like in this case, a separate job queue is needed to check for an abandoned cart and send an abandoned cart email) This isn't the only way you can build a RESTful API with Temporal, but this pattern works well if you use long-lived Workflows to store user data. Next up, we will integrate this partner backend with hydrogen (without the external payment we created in this part, of course).

Via Backend
Facebook
Twitter
Telegram
WhatsApp
Older Posts No results found
Newer Posts

You may like these posts

Post a Comment

Ads Single Post 4

Faiza Tanjia

About Us

Hi, I am Faiza, one of the team leaders here at MyCovers. We are passionate about Dynamic eCommerce and love sharing our knowledge and research with you. At MyCovers, we strive to be the ultimate resource for learning everything Covering Dynamic eCommerce Application Development!

Featured Post

Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

Faiza Tanjia- May 11, 2022
Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

May 11, 2022
Shopify Hydrogen App Development Tutorial 101 Part 3 - Partner App Backend Integration using Temporal.io (Shopping Cart and Payment - ep 2)

Shopify Hydrogen App Development Tutorial 101 Part 3 - Partner App Backend Integration using Temporal.io (Shopping Cart and Payment - ep 2)

May 10, 2022
Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)

Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)

February 07, 2022
Headless eCommerce: from a Complementary to Elementary Disruptive Business Strategy

Headless eCommerce: from a Complementary to Elementary Disruptive Business Strategy

February 17, 2022

Editor Post

Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)

Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)

February 07, 2022
Headless eCommerce: from a Complementary to Elementary Disruptive Business Strategy

Headless eCommerce: from a Complementary to Elementary Disruptive Business Strategy

February 17, 2022
Shopify Hydrogen App Development Tutorial 101 Part 3 - Partner App Backend Integration using Temporal.io (Shopping Cart and Payment - ep 2)

Shopify Hydrogen App Development Tutorial 101 Part 3 - Partner App Backend Integration using Temporal.io (Shopping Cart and Payment - ep 2)

May 10, 2022

Popular Post

Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

Shopify Hydrogen App Development Tutorial 101 Part 4 - Partner App Backend Integration using Temporal.io (Partner Backend API - ep 3)

May 11, 2022
Shopify Hydrogen App Development Tutorial 101 Part 3 - Partner App Backend Integration using Temporal.io (Shopping Cart and Payment - ep 2)

Shopify Hydrogen App Development Tutorial 101 Part 3 - Partner App Backend Integration using Temporal.io (Shopping Cart and Payment - ep 2)

May 10, 2022
Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)

Shopify App Development using React based Hydrogen Framework: A Comprehensive Tutorial 101 (part 1)

February 07, 2022

Popular Categories

  • Backend 3
  • Business Strategy 1
  • Headless eCommerce 5
  • Hydrogen 4
  • React 1
  • Tailwind 1
MyCovers, covering dynamic ecommerce

MyCovers

Your ultimate resources for learning everything about Dynamic eCommerce Application Development

Follow Us

Copyright © 2022 MyCovers

  • About
  • Contact Us
  • Accessibility
  • Privacy Policy
  • Disclaimer