Sitemap

Payment Link Strapi plugin Part 1 — The server

Step by step, one feature after another.

11 min readApr 14, 2022

--

Strapi is a headless CMS. It means one can easily create a backend site with a useful API. It has a decent plugin API, but documentation around it could’ve been more detailed.

I’m writing this article to synthesize my learning path. Feel free to write issues in Github repo if I made a mistake or took a not needed detour.
I don’t recommend to follow these steps blindly — I’m learning too.

We will write a plugin for creating a payment link with PayU. Please refer to the PayU REST API documentation for more information about the service. Please also register to PayU or use the public sandbox configuration if you want to directly follow the article and run examples.

Installation process

First we will prepare the source with automatic tools. To install an empty Strapi project:

yarn create strapi-app my-project --quickstart

A lot of content has been created. A server started running and a website was opened in the browser. Let’s kill the server for now and navigate into the directory.

cd my-project

To bootstrap a plugin run yarn strapi generate, navigate to the type of content you’d like to generate (“plugin”) and write a name of the plugin. I’ve chosen to name it payu and I will use this name in the article.

yarn strapi generate

The command created the /src/plugins/payu directory. It contains admin and server directories to declare the plugin’s admin interface and the API. Their contents will be explained as we go.

The payu directory contains the pre-configured package.json and is ready to become a separate project. You can init your favorite version control service in the plugins/payu directory. It will become for us a relative root as we continue.

The Content Types

Strapi’s backbone idea is using Content Types to define database and API. For the purpose of this plugin we will use two kinds of them — singleType and collectionType.

The singleType kind is used for a singleton containing fields. We will use it to create plugin’s settings.

The collectionType kind is a definition of columns in database. We will use it to store transactions.

Configuration

Storing settings in database

To be able to allow the user to easily edit the merchant settings, we need to store them in the database. We will need to store client id and secret, URLs to authorization system and to the API, we will also store the notify URL to receive the webhooks from PayU.

Strapi created the server/content-types directory with one file in it, index.js. Let’s create the server/content-types/production-settings.js file with the following content:

We’ve chosen to make the collection a singleType kind as we need only one item.

singularName: "production-settings",

We will use this name to collect the settings later in other parts of the plugin.

pluginOptions: {
"content-manager": { visible: true },
"content-type-builder": { visible: false }
},

We temporarily allow the settings to be visible in the Content Manager, so it will be possible to save them from the admin panel. We will switch it off later when we will edit the admin side of the plugin. We will also require the user to have certain privileges to be able to change/view these settings.

At last we’ve defined attributes — fields we will use to authenticate within PayU and the addresses to be used.

To make Strapi see the provided schema we need to modify the content-types/index.js file as well.

// server/content-types/index.js
'use strict';
const productionSettings = require("./production-settings");module.exports = {
"production-settings": productionSettings,
}

Adding sandbox environment

We will add another configuration Content Type to make it possible to choose between production and sandbox environment.

The payu/server/content-types/sandbox-settings.js file is very similar to the production one except of the default URLs, clientId and clientSecret:

And the index.js will now have both content-types loaded:

// server/content-types/index.js
'use strict';
const productionSettings = require("./production-settings");
const sandboxSettings = require("./sandbox-settings");
module.exports = {
"production-settings": productionSettings,
"sandbox-settings": sandboxSettings,
}

We still can’t do anything except of filling in the details as for now. Let’s do it. For the notifyUrl I usually create an entry on webhook.site so it will be possible to receive the webhooks from PayU and see them in real time even when operating from the localhost.

Calling strapi.query("plugin::payu.production-settings").findOne() is a way to retrieve the object from database.

In file server/content-types/configuration.js we will create an ability to choose between sandbox and production environment

We will now connect it in the server/content-types/index.js

// ...
const configuration = require("./configuration");
module.exports = {
// ...
configuration,
};

The Configuration object should be stored in database from the first use of the plugin. We will use the bootstrap phase to create one if needed.

Press enter or click to view image in full size
PayU plugin configuration with production/sandbox choice
Press enter or click to view image in full size
PayU marchant sandbox settings

Please fill in the details from PayU service leaving the notifyUrl blank as we haven’t build that functionality yet.

Creating a very unsafe endpoint

We will now define the first temporary API endpoint and see if we’re able to authorize within PayU authentication system. The goal is to receive the authorization token, cache it and display. We will use it later to make all requests to PayU services.

The plugin’s bootstrap contains the server/router, server/controllers and server/services directories.

Since beginning they’re connecting the plugins default API endpoint http://localhost:1337/payu/ with the server/services/my-service.js to display plain text message:

$ curl localhost:1337/payu
Welcome to Strapi 🚀

You might see an UnauthorizedError. To allow an unauthorized access, change the route of the plugin — in file server/router/index.js add auth: false, to the config: section.

Let’s change it to call the PayU authentication server and receive the token.

We will need to make requests. I like axios. Use any library you like.

yarn add axios

Let’s create server/utils directory with a payu.js file to abstract the PayU code from the service.

We will add a getToken method to the services/my-service.js to create a new temporary service. The file will now look as below:

We need also to change the service/controllers/my-controller.js controller to work in async/await mode and use the getToken service.

module.exports = {
async index(ctx) {
ctx.body = await strapi
.plugin('payu')
.service('myService')
.getToken();
},
};

If all goes well, we will now see the PayU token as a response to the plugin’s API endpoint request.

$ curl localhost:1337/payu
{"token":"abf39b7a-78ee-4259-ac92-25c10e9c4000"}

Please remember this is not safe to leave this endpoint in the code. Change the my-controller.js file getToken to former getWelcomeMessage. We will soon rename/delete the file as well.

Refactoring phase

We’ve proven that our attempt is working. It’s time to organize things for the future and clean up some automatically generated features. We’ve got the getToken functionality mixed with retrieving settings in one function in my-service.js. We will:

  • Create service server/services/settings.js to retrieve settings from database and check if these are actually set
  • Create service server/services/auth.js where we will get the token
  • Remove the server/controllers/my-controller.js
  • Remove the server/service/my-service.js
  • Disable routing in server/routes/index.js

Create the settings service

Create the authentication service

Add them to the server/services/index.js

'use strict';const auth = require('./auth');
const settings = require('./settings');
module.exports = {
auth,
settings,
};

At this point you still may check if server is working by changing the server/controllers/my-controller.js file to call the right service.

// ...
ctx.body = await strapi
.plugin('payu')
.service('auth')
.getToken();
// ...

We will now remove the route entry in services/routes/index.js

// server/routes/index.js
'use strict';
module.exports = [];

Then remove the my-controller entry from server/controllers/index.js

// server/controllers/index.js
'use strict';
module.exports = {};

Remove the mentioned files (my-controller.js and my-service.js). We are no longer able to see anything on the old route:

$ curl localhost:1337/payu
{
"data":null,
"error":{
"status":404,
"name":"NotFoundError",
"message":"Not Found",
"details":{}
}
}%

Payment link

Defininig the payment link flow

Our plugin’s payment_link API will receive data from POST request contact PayU service to receive the actual payment link and then redirect the browser so the user could pay with any payment method.

Sequence diagram of payment-link flow

Storing transactions

Before sending the request to create an order on the PayU side, we will need to create the Transactions collectionType. It will be later updated with information about the order and PayU response. PayU is collecting information about the user (IP address, buyer information), order (products, amount and currency) and the transaction itself (id). It is also sending us some information about the transaction on PayU side — redirect URI and the id. We will store this information along with the status. Let’s define it in the server/collection-types/transactions.js file.

There are some differences comparing to the models provided before.

  • kind: "collectionType" means that we might, and most probable will, store more than one Transaction object.
  • options: { draftAndPublish: false } , options are required for the collectionType and since we don’t want to the draft feature in the entries, we’ve disabled the draftAndPublish
  • uid attribute is needed to use the public sandbox, the standard integer key didn’t made the transaction unique in PayU database
  • status attribute. The names of the status (except of the CREATED one) are reflecting the statuses provided by PayU

We need to add the Transactions collection to the system by editing the server/collection-types/index.js and referring to the collection by its singularName .

// ...
const transaction = require("./transactions");
module.exports = {
// ...
transaction,
};

Sending request to PayU API

We will use the already created server/utils/payu.js file and add the payu.createOrder() function. It needs settings, token and the transaction already stored in database (containing the uid).

The createOrder() function is called with the settings, token and transaction object. We’re building the body for the request, first with the required and later adding the optional fields. If buyer is not provided PayU will present a form to submit the email address to the User. It will be later returned in the webhook.

PayU order creation endpoint is redirecting to the redirectUri, hence the additional option “maxRedirects”: 0 , and the unusual data retrieval. The response is not 200 and axios throws an error.

Create service

To be able to use the payu.createOrder() function from within the plugin we will create the service in server/service/payment-link.js file. It will create a Transaction and use the created object to payu.createOrder(). Then it will read the response and update the transaction with the retrieved payment link and status.

And register the service in plugin

// server/services/index.js
// ...
const paymentLink = require('./payment-link');
module.exports = {
// ...
paymentLink,
};

Payment link API endpoint

The service is ready. Let’s connect it to the endpoint. First we will create the createTransaction() controller in server/controllers/payment-link.js

And then we’ll connect it in the server/routes/index.js

We’ve defined a POST request. Any other will result with either 401 or 404 response. Let’s send a request within minimal data using curl

$ curl \
-X 'POST' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"currencyCode":"PLN",
"totalAmount": "1000",
"description":"test",
"customerIp": "127.0.0.1"
}' localhost:1337/payu/transactions
{

This returns a JSON response

Frontend will receive the response and will use the redirectUrifield to finalize purchase process. This will be presented as a payment link. When loaded, the page would be already rendered by PayU.

Press enter or click to view image in full size
PayU payment page

The recipient is “www.payu.pl” as we’ve used the public sandbox. It’s easy to “pay” if you are using a sandbox environment. Just use one of the cards provided in the sandbox cards. The card with a number 4444333322221111 expiry date 12/29 and 123 as CVV is given for successful transactions.

Handling webhooks

PayU is communicating with the world by sending webhooks requests. After any action on the payment page we will receive a webhook request to the provided notificationUrl.

Press enter or click to view image in full size
Webhook handling sequence diagram

Watching the webhook requests as they happen

We can watch the webhooks using a webhook.site by adding its URL in the notifyUrl field of the settings.

The successful payment flow will generate two webhook POST request.

The first webhook is about switching the PayU order to the PENDING state, just after the PayU payment page has been displayed.

The second — order COMPLETED, after payment is authorized.

We haven’t provided the buyer when creating the transaction with curl. Data in the buyer field was provided during the PyU payment process. Depending on the payment type it might be just an email. We will store this data in transaction collection.

It’s not necessary to write any function in payu.js, we will continue in the same order as above — service, controller, route.

In server/services/payment-link.js we will add the handleWebhook() function

In server/controllers/payment-link.js we will add the handleWebhook() controller

//...
async handleWebhook(ctx) {
ctx.body = await strapi
.plugin('payu')
.service('paymentLink')
.handleWebhook(ctx.request.body);
},
// ...

In server/routes/index.js we will add the notify route

// ...
{
method: 'POST',
path: '/notify',
handler: 'paymentLink.handleWebhook',
config: {
auth: false,
policies: [],
},
},
// ...

We will now check if our webhook is working by passing a webhook request to the URL (data is copied from webhook requests collected earlier on the webhook.site).

$ curl \
-X 'POST' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"order": {
"orderId": "TFJBNPX6ZS220409GUEST000P01",
"extOrderId": "24c6d9b2-c707-4d2a-8887-3991b903b3f6",
"orderCreateDate": "2022-04-09T11:20:59.521+02:00",
"notifyUrl": "https://webhook.site/5e25fb1d-a0cb-47ef-b8f7-f71ca7f7d5cc",
"customerIp": "127.0.0.1",
"merchantPosId": "300746",
"validityTime": "86400",
"description": "test",
"currencyCode": "PLN",
"totalAmount": "1000",
"status": "PENDING"
},
"properties": [
{
"name": "PAYMENT_ID",
"value": "5003707594"
}
]
}' http://localhost:1337/payu/notify

Responses with the updated transaction:

We can see the changed status and properties

When we request the COMPLETED webhook

curl \         
-X 'POST' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"order": {
"orderId": "TFJBNPX6ZS220409GUEST000P01",
"extOrderId": "24c6d9b2-c707-4d2a-8887-3991b903b3f6",
"orderCreateDate": "2022-04-09T11:20:59.521+02:00",
"notifyUrl": "https://webhook.site/5e25fb1d-a0cb-47ef-b8f7-f71ca7f7d5cc",
"customerIp": "127.0.0.1",
"merchantPosId": "300746",
"validityTime": "86400",
"description": "test",
"currencyCode": "PLN",
"totalAmount": "1000",
"buyer": {
"customerId": "guest",
"email": "john.dpe@example.com",
"firstName": "John",
"lastName": "Doe"

},
"payMethod": {
"type": "CARD_TOKEN"
},
"status": "COMPLETED"
},
"localReceiptDateTime": "2022-04-09T11:21:36.265+02:00",
"properties": [
{
"name": "PAYMENT_ID",
"value": "5003707594"
}
]
}' http://localhost:1337/payu/notify

The response is the updated transaction again (here only the changed fields)

{
// ...
"buyer":{
"customerId":"guest",
"email":"john.doe@example.com",
"firstName":"John",
"lastName":"Doe"
},
"status":"COMPLETED",
"payMethod":{"type":"CARD_TOKEN"},
// ...
}%

It’s now possible to publish the site to a public URL and use the notify address {host}/payu/notify as the notificationUrl in the PayU settings.

What’s next?

The plugin is functional, but far from finished. I’d like to register a hook to a completed purchase. Like sending a thank you email to the buyer and a notification one for the owner. I’d like to be able to restrict access to the settings and transactions. Extract the daily list of completed transactions.

One thing I haven’t presented here is testing the code. This is because the changes would require modification of the files outside of the plugins/payu directory

In the next parts I’d like to write about hooks, securing the system, and configuring the admin side of the plugin.

Please feel free to leave the comment here.

--

--

Piotr Zalewa
Piotr Zalewa

Written by Piotr Zalewa

Creator of JSFiddle, ex-Mozilla dev. Software consultant & mentor. I code and write about programming, mostly Python. Open to diverse technologies.

No responses yet