collana pay Integration Guideline
This document describes the integration of Collana Pay into a shop system. It does not provide specific instructions for a particular shop system but outlines general principles and possible solutions. The second part of this guideline illustrates a potential concrete implementation using the "Shopware" shop software as an example.
However, the primary goal of this document is to familiarize the developer with the workings of Collana Pay, its advantages over other solutions, and best practices for its utilization.
collana pay
The underlying idea of Collana Pay is to provide a universal interface to various Payment Service Providers (hereinafter referred to as PSPs) and thus become independent of the specific peculiarities of different PSPs. This approach offers two immediate advantages to the shop operator:
- The use of an additional or different PSP is very straightforward since the same API can continue to be used. The selection of a specific payment method is done through an HTTP header field. Switching to a different PSP for the same payment method requires no changes in the shop system. The PSP for the payment method is determined in Collana Pay.
- Changes to the APIs of the PSPs do not necessarily need to be transferred to the integration in the shop system. Collana Pay functions as middleware and takes on this task. Consequently, the use of Collana Pay is significantly more maintenance-friendly than direct integrations.
Without Collana Pay, online shops must integrate multiple PSPs, and when switching between PSPs, they also need to switch the integration:
Collana Pay takes over this task, so that online shops only need to integrate a single interface.
Asynchrony
Asynchrony is a specific feature of Collana Pay that advances the established concept of IPN (Instant Payment Notification) within electronic payment processing and builds the entire communication framework around it. IPN is based on the optional ability to establish an asynchronous channel in addition to synchronous communication. Through this channel, Payment Service Providers (PSPs) can send additional information, such as updates and status notifications, to the online shop. In the case of Collana Pay, asynchrony is no longer an option but the foundation of communication. Although this represents a departure from familiar procedures in some areas, this architecture brings several advantages.
Asynchronous systems are inherently more robust as they do not rely on stable network connections like traditional synchronous communication. There is a reversal of dependency, so that network problems do not need to be addressed separately; instead, responses are triggered only by incoming requests.
Furthermore, asynchronous systems support the observed trend towards Microservices, which already advocates for a stricter and more robust separation of individual system components. Particularly in the web environment, with the progression of modern solutions like PWA (Progressive Web Apps) and Websockets, asynchronous procedures showcase their strengths.
REST-API
General
Collana Pay provides a generic REST API that operates independently of the specific Payment Service Providers (PSPs) behind it, enabling the flexibility mentioned earlier.
Documentation
The API documentation is outlined by Collana Hive through OpenAPI and is accessible online via Swagger or ReDoc.
https://sandbox.collanapay.com/swagger/index.html#/
https://sandbox.collanapay.com/redoc/index.html?url=/swagger/v2/swagger.json
Usage
The REST API operates with familiar HTTP verbs. Authentication is done through an API key, which is sent in the HTTP header with each request. JSON is used as the transport format, known for its lower overhead and native support for various data types compared to XML.
Development
As REST is built on simple, established technologies, the API can be accessed in principle using basic tools like curl in the terminal. However, there are some tools that can significantly simplify the development process.
We recommend using the following tools:
- Postman is a convenient HTTP client designed specifically for working with APIs. More information can be found at postman
- Webhook.Site is a service for analyzing incoming HTTP requests, making it ideal for working with asynchronous systems. More information is available at webhook
Auch wenn wir diese Werkzeuge verwenden, so sind sie nicht notwendig, um die Integration von collana pay vorzunehmen.
Procedure
Regardless of the deployed shop system and, consequently, the programming language in which it is written, the following are the necessary sub-steps for implementation.
However, references to the implementation example will be provided intermittently.
General Process:
- The customer chooses to process the payment with Collana Pay. It is not necessary for Collana Pay to be visible to the customer; instead, the payment method is presented to the customer simply as a payment processing option.
- The shop system communicates with Collana Pay via REST and creates a new transaction there. As a response, the shop system receives a unique
transactionId
- Using this
transactionId
, the shop system can now communicate with Collana Pay and trigger further steps known as interactions. Each interaction is identified by a unique interactionId. Responses to these interactions are received asynchronously and are always marked with the overarchingtransactionId
and the correspondinginteransactionId
. - The customer's subsequent actions no longer occur with the shop system but with Collana Pay or the Payment Service Provider (PSP). The response from Collana Pay contains a URL to which the customer is redirected. The customer enters the necessary processing data (e.g., credit card information) there and is then redirected back to the shop.
- The shop system processes the asynchronous response to the customer's input at Collana Pay.
It is not crucial at which point the shop system places the payment processing in the checkout process. This process is self-contained.
Terminology
Transaction
The term transaction refers to the complete processing of a payment within Collana Pay. Even if multiple steps are undertaken, and various subtasks need to or can be performed, all of these occur under a unified transaction ID.
Interaction
A transaction consists of multiple sub-steps, known as interactions. An example of an interaction is a
reservation
. Each interaction is identified by its own interaction IDinteractionId.
Frontend
The exact definition of the term "frontend" (as well as its counterpart "backend") is not consistent and varies depending on the environment and type of application. For the context of this document, we refer to the frontend as the client-side code delivered and executed by an E-commerce solution.
In most cases, this will be a (at least partially) server-side rendered HTML document, which can perform various tasks (especially UI logic & validation, but also communication) using scripting languages, typically JavaScript.
The frontend is not under the immediate control of the provider but is entirely executed in the user's environment, usually a web browser. For this reason, developers in the frontend must consider some specifics. These are not specific to Collana Pay but are generally applicable:
- The availability of capabilities can only be assumed to a limited extent. Developers should define a minimum set of requirements for themselves or in coordination with the client or operator, and communicate this to the end user. It is crucial to strike a balance between broad acceptance and development/maintenance effort. An application that entirely avoids JavaScript may work seamlessly on any device but may lack significant convenience features.
- The stability of the application is subject to much greater uncertainties, and unlike the server-side component, there are often significantly fewer options for logging and debugging. In particular, the runtime environment must be considered from entirely different perspectives. Available resources (especially processor performance), network performance (bandwidth), and similar factors undergo significant fluctuations and must be tested much more intensively.
-
The security of the application cannot be guaranteed. All data received from the client must be considered insecure. This includes not only the commonly validated data received via POST or as GET parameters but also data from the HTTP header, especially cookies, which can be easily manipulated. Client-side validation can be used to enhance user experience and alleviate the server-side application, but it never replaces server-side validation. Availability concerns, as mentioned earlier, must also be taken into account. The use of modern HTML5 input types such as
date
ornumber
or functional attributes likerequired
may not be interpreted at all.
Therefore, an important note: For the sake of clarity, we are omitting many security-critical checks, error-handling, and stabilizing fallback and polyfill functions in this document. Developers are encouraged to implement these based on their own environment, agreements, and requirements.
Backend
Analogous to the previous definition of the frontend, we refer to the server-side application of an E-commerce solution as the backend. The backend performs the business logic and communicates with the frontend by providing the same and offering URL-based interfaces, from which the frontend can fetch data or to which it can send data.
Many e-commerce systems often designate the administrative area of the system as the backend. We do not adopt this interpretation here, but explicitly mention it to avoid confusion. The specific capabilities of the deployed e-commerce system and configuration functions are not considered in this document.
Integration of Collana Pay as a Payment Method
The following flowchart provides an overview of the fundamental process of a payment using collana pay, involving all three components (Frontend, Backend, collana pay):
The detailed steps
Following this theoretical consideration, now we will outline the specific individual steps that constitute the processing of a payment through collana pay:
In this representation, Frontend and Backend are considered as a unified entity called "Shop."
As depicted, a total of 3 steps are undertaken to complete the payment process in the shop. These are detailed as follows:
create
Route: /transactions
Is used to create a transaction.
-
Synchronously, the
transactionId
and aninteractionId
are returned. - Asynchronously, the confirmation and summary of the transaction are delivered.
prepare
Route: /transactions/{transactionId}/prepares
Is used to prepare a payment.
-
Synchronously, the
transactionId
and aninteractionId
are returned. -
Asynchronously, the confirmation and summary of the transaction are delivered, and optionally, a RedirectUri or EmbedmentUri may be provided, which is to be displayed or
RedirectUri
oreEmbedmentUri
, may be provided, which is to be displayed or redirected.
reservation
Route: /transactions/{transactionId}/reservations
may be provided, indicating where the information is to be displayed or redirected.
-
Synchronously, the
transactionId
and aninteractionId
are returned. -
Asynchronously, the confirmation and summary of the transaction are delivered, and optionally, a
RedirectUri
orEmpedmentUri
, may be provided, which is to be displayed or to which the end customer should be redirected.
Additional Actions
As evident in the documentation, there are additional routes for various tasks. However, these tasks can, in principle, be performed by ERP systems or the shop system in the subsequent order process and are not strictly necessary for payment processing within the order process in the online shop. Therefore, they are not covered here.
However, they all operate according to the procedure described here and can be implemented just as easily.
Creating a Transaction
This is the step where the processing of "traditional" payment methods begins as well. The user has selected the payment method in the frontend and, through some interaction, such as clicking a "pay" button, initiated the payment process. In the backend, the processing of the transaction now begins.
Due to the architecture of Collana Pay and the use of asynchronous communication, it is necessary to establish a way to globally track and associate asynchronous responses. This task is handled by the
transactionId
, which needs to be created now. This is done through a POST request to /transactions
:
curl -H "Content-Type: application/json"
-H "Accept: application/json"
-H "ApiKey: xxxx-yyyy-zzzz-0000"
-H "x-payment-method-type: CreditCard"
-d '{"key1":"value1", "key2":"value2"}'
-X POST https://collana pay/api/v2/transactions
Let's take a closer look at the request:
-H "Content-Type: application/json"
-H "Accept: application/json"
Here, it is specified that the transmitted data is sent in the JSON format (Content-Type
) and the response from Collana Pay is also expected in this format (Accept
).
-H "ApiKey: xxxx-yyyy-zzzz-0000"
This is the login by which the backend authenticates itself with Collana Pay. The API key is provided by collana hive.
-H "x-payment-method-type: CreditCard"
This header defines the payment method. The payment method is a fixed, predefined value (e.g. CreditCard
, PayPal
, PayPalExpress
, KlarnaSofort
, DirectDebit
, Terminal
, IDeal
, Invoice
) that is independent of the Payment Service Provider (PSP). The complete list is provided by collana hive.
-d '{"key1":"value1", "key2":"value2"}'
This is the content of the HTTP body ('payload') that is sent via POST. The precise definition of the payload can be found in the documentation (refer to 'REST API Documentation').
The response to this request contains the transactionId
, which is necessary for all further communication. Therefore, it is essential to store this in a form within the shop system that is globally accessible, for example, in a dedicated database table. In the implementation example, the table shopware.flenocollanapay_payments
is used for this purpose.
This step can be processed synchronously, meaning that the response to the request can be directly handled since there is no communication with the Payment Service Provider (PSP) yet, and comprehensive validation checks the transmitted data. If there is no HTTP error, the creation was successful. For completeness, we recommend, however, as with other calls, to wait for the asynchronous response and process it based on its status.
Processing a Transaction
In the next step, the actual payment processing is initiated. Now, several involved components need to be synchronized:
- The frontend, i.e., the customer's browser
- The backend, i.e., the shop system
- Collana Pay
Traditionally, communication between 1 and 2 occurs synchronously, meaning that a user's action triggers a response from the shop system, which typically responds with a new resource or a redirect. In addition, communication between 2 and 3 is now asynchronous, meaning that the shop system sends a request to Collana Pay but receives the response asynchronously via a callback URL. The synchronous response only contains information about whether the request was accepted, along with the transactionId
& interactionId.
,as mentioned earlier."
To address this, we recommend establishing a two-tiered architecture that places the frontend at the center. Upon completion of Step 1, the user is redirected to a page within the shop system where all subsequent communication steps are coordinated. In parallel, the shop system constructs its own internal 'Middleware' that represents the current state of the payment and shields the frontend from Collana Pay.
Hence, there is no direct dependency between the frontend and Collana Pay. Instead, the backend provides a means to derive a synchronous representation for the frontend from the asynchronous communication with Collana Pay. The frontend, in turn, will quasi-real-time depict state changes, utilizing asynchronous technologies.
For this purpose, the frontend establishes an asynchronous connection to a local endpoint of the backend. For compatibility reasons, a Long Polling solution is most suitable in this context. While an architecture based on Websockets would be ideal, implementing this would require significant restructuring for most common shop systems and their server architecture.
Integration im Frontend
The frontend establishes a connection to a backend endpoint using Long Polling. The purpose of this connection is to synchronize the current state of the transaction between the frontend and backend.
Integration im Backend
The backend provides two endpoints, one for reading (GET) and one for writing (POST).
The GET endpoint is used by the frontend for communication and essentially provides information about the current state of the transaction. The POST endpoint serves as the target for asynchronous responses from Collana Pay.
Additionally, the backend has a method for persistently storing the payment state. The payment state essentially comprises three pieces of information:
-
The unique identifier (
transactionId
) -
A code for identifying the state based on the interaction (
status
) - A payload, necessary depending on the current state, such as a redirect URL.
In most cases, a database table will be used for this purpose, and this guide assumes this in its code examples.
In the first step, the transaction was created, meaning the transaction is in the state of
create.done
. This naming is arbitrary and can be adjusted by the developer; it is used here only as an example.
Zusammenspiel der Komponenten
Nun betrachten wir, wie die eingangs genannten 3 Komponenten (Frontend, Backend, collana pay) miteinander interagieren.
Progeression in the front end
The frontend maintains continuous communication with the backend. It is aware of how to react at each state. This ensures that the user remains consistently informed about the current status of their payment processing, preventing any perception of sluggishness or system failure.
Upon receiving immediate information that the transaction is in the create.done
state, the frontend communicates with the backend via AJAX to represent the current state. The backend, in turn, communicates with Collana Pay and updates the respective stages in the current local state.
The Activity of the Backend
After creating the transaction and processing the response, the backend has established the local state. Additionally, it has directly initiated the next processing step (prepare
). To do this, it sends the next request to Collana Pay, instructing the system to prepare the payment.
curl -H "Content-Type: application/json"
-H "Accept: application/json"
-H "ApiKey: xxxx-yyyy-zzzz-0000"
-H "x-payment-method-type: CreditCard"
-d '{"key1":"value1", "key2":"value2"}'
-X POST https://collanapay/api/v2/transactions/1234-5678-9101-2131/prepares
The individual components of the request have already been addressed. Of particular note this time is the destination of the request:
-X POST https://collanapay/api/v2/transactions/1234-5678-9101-2131/prepares
In addition to the payload in the HTTP body (-d
) data is now also transmitted in the URL. This is done within the resource, not as query
-parameters. DThe URL contains a dynamic part for transmitting the transactionId
: https:/collanapay/api/v2/transactions/{transactionId}/prepares
. This method is also used for all other routes (e.g., reservations, captures, etc.).
collana pay can now respond to this HTTP request in two ways:
- The response is a success code. In this case, the request was accepted, and Collana Pay will send the response to the previously defined callback address.
- The response is an error code. This code must be interpreted. The result may either indicate that a new attempt should be made, e.g., in case of network problems. In this case, the state is simply reset to the previous one, and a new attempt is automatically made. Alternatively, the result may indicate more significant issues, in which case the transaction must be aborted, and the customer needs to use an alternative payment method.
Depending on the response from Collana Pay, the new state is now stored locally and is either prepare.send or prepare.error.
In the meantime, the frontend has received the new state. In the case of prepare.send, for example, a progress bar can be updated, or a text message can be displayed. In the case of an error, the customer needs to be informed and redirected back to the payment method selection.
The next step
Meanwhile, Collana Pay has completed the prepare step and informed the shop system via the callback URL. This triggers an update of the state, which is now prepare.pending. The frontend receives this information and acts exactly as described in the previous section.
Additionally, Collana Pay has provided a URL. This URL offers a form for the customer to enter their data. This URL is also stored in the local state and transmitted to the frontend, which displays the URL in an iframe or redirects to the corresponding URL.
Frontend response
As described earlier, the frontend has quasi-real-time knowledge of the payment state. The current state includes the transmitted URL, which the frontend displays in an iframe while the state comparison continues in parallel.
When the user enters and submits their data within the iframe in the frontend, Collana Pay informs the backend via the known callback address. The backend updates the payment state, and the frontend is informed in the usual way.
Subsequently, after the reservation step, Collana Pay transmits a redirect URL to the backend, which is then passed on to the frontend. The frontend will redirect the customer to this URL, completing the processing of the payment on the frontend side.
Transaction Completion
In the previous step, the customer was redirected to Collana Pay, which then takes over the further processing of the payment. Upon completion of the processing, the customer is redirected to the previously defined redirect URL of the shop.
This is typically the familiar frontend page, which now resumes its task of informing the customer about the current state of the payment processing.
Simultaneously, the backend receives asynchronously transmitted information from Collana Pay confirming that the payment has been completed.
Implementation
After this theoretical examination of the process, it will now be illustrated through a real implementation. The basis is a payment plugin designed for the Shopware 5 e-commerce platform.
Please note that the code shown here is not complete. It has been abbreviated to focus on the Collana Pay process.
Backend
The initiation of the payment process depends on the software being used. In this example, for illustrative purposes, code from the implementation for the Shopware e-commerce platform is used, where a payment begins with the invocation of a controller route, as shown below:
class Shopware_Controllers_Frontend_CollanaPay extends \Shopware_Controllers_Frontend_Payment
{
public function createAction()
{
$body = [
'amount' => $this->getAmount(),
'currencyCode' => 'EUR',
'paymentCountry' => 'DE',
'billingCustomerData' => $this->getAddress(),
'shippingCustomerData' => $this->getShippingAddress(),
'baseUrl' => Shopware()->Shop()->getBaseUrl() . $this->Front()->Router()->assemble([
'controller' => 'collanapay',
'action' => 'return'
])
];
$response = \FlenoCollanaPay\Api::CreateTransaction($body);
$payment = new \FlenoCollanaPay\Models\Payment\Payment();
$payment->transactionId = $response['transactionId'];
$entityManager = $this->container->get('models');
$entityManager->persist($payment);
$entityManager->flush();
return $this->redirect($this->Front()->Router()->assemble([
'controller' => 'collanapay',
'action' => 'process'
]);
}
}
This creates a new payment and stores thetransactionId
in a database table.
Note: The Shopware e-commerce system follows the MVC (Model-View-Controller) pattern. While not a part of this guide, here's a brief overview for better understanding. MVC is a pattern for dividing software into three components: the data model (model), presentation (view), and program control (controller). The degree of interdependence between these components varies depending on the implementation.
- Model - The model contains data represented by the presentation. It operates independently of the presentation and control. Changes in the data are communicated to the presentation through the "Observer" design pattern. In some implementations of the MVC pattern, the model includes business logic responsible for modifying the data.
- View - The presentation is responsible for displaying the model's data and implementing user interactions. It is aware of the model whose data it presents but is not responsible for processing this data. Additionally, it operates independently of the control.
- Controller - The controller manages the presentation and the model. It is informed about user interactions, interprets them, and then makes adjustments to the presentation as well as changes to the data in the model.
The general process is straightforward and easy to understand. The payment processing is initiated with an entry point defined by the e-commerce system, triggering the start of the payment handling with Collana Pay and delivering a corresponding frontend to the customer.
Special attention should be given to two aspects in this context:
-
The invocation of
\FlenoCollanaPay\Api::CreateTransaction()
, which encompasses the sole synchronous communication with Collana Pay. - The definition of the callback URL for collana pay. This URL facilitates asynchronous communication.
We will now delve into both aspects in more detail.
1. CreateTransaction()
namespace FlenoCollanaPay;
use GuzzleHttp\Client;
class Api
{
/**
*
* @param array $body
*/
public static function CreateTransaction(array $body)
{
$client = new Client();
$response = $client->post(sprintf('%sapi/v1/transactions', self::getCollanaPayEndpoint()), [
'body' => json_encode(self::prepareBody($body), JSON_UNESCAPED_SLASHES),
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'ApiKey' => self::getApiKey()
]
]);
if($response->getStatusCode() !== 202)
{
throw new \Exception('invalid status code');
}
return json_decode($resp->getBody(), true);
}
/**
*
* @param array $body
*/
private static prepareBody(array $body)
{
return array_merge([
'callback' => [
'uri' => [
'type' => 'HttpJson',
'uri' => sprintf('%sapi/collanapay', \FlenoCollanaPay\Api::getBaseUrl())
]
]
], $body);
}
}
The structure of this method is remarkably simple and focused on the essentials. Only hinted at is error handling, which responds to all HTTP status codes except 202
with an exception. In reality, a more detailed error handling approach is highly recommended.
It is particularly advisable to differentiate between HTTP status codes 200
, 300
, 400
und 500
, to respond to specific potential sources of errors. Logging, especially in case of errors, is also recommended. However, since these are general requirements for developing an API interface and not specific to Collana Pay, mentioning it here suffices for this case.
2. Callback-URL
The callback URL provided in the body serves as the central communication point between the backend and Collana Pay. In this implementation, the callback URL is example.com/api/collanapay
. An collana pay will send all asynchronous responses to this URL.
So, the implementation is expected to perform two tasks:
- Receive and process HTTP requests from collana pay.
- Trigger the next sub-step in response to an asynchronously received request, if necessary.
Regarding task 2, this approach has the advantage of ensuring no processing delays since the backend's response occurs directly without detours.
From the backend's perspective, the callback URL is passive, as it merely responds to asynchronous replies from Collana Pay. Technically speaking, an asynchronous response in the HTTP protocol is not explicitly defined; instead, it involves as POST
-request initiated by Collana Pay, executed on the application side by Collana Pay but triggered previously by the backend.
How such a callback URL can be implemented is demonstrated in the following code example:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* POST-request an /api/collanapay
*/
public function postAction()
{
$body = json_decode($this->request->getRawBody(), true);
$transactionId = $body['transactionId'];
$repository = $this->container->get('models')->getRepository(\FlenoCollanaPay\Models\Payment\Payment::class);
$payment = $repository->findOneBy([
'transactionId' => $transactionId
]);
$body['StatusDetails'] = explode('; ', $body['StatusDetails']);
switch ($body['EndpointType'])
{
case 'Transactions':
if($body['StatusDetails'][0] == 'CreateInteraction')
$this->processCreateInteractionResult($payment, $body);
break;
case 'Prepares':
if($body['StatusDetails'][0] == 'PrepareInteraction')
$this->processPrepareInteractionResult($payment, $body);
break;
case 'Reservations':
if($body['StatusDetails'][0] == 'ReservationInteraction')
$this->processReservationInteractionResult($payment, $body);
break;
}
$this->response->setStatusCode(200);
$this->response->setBody(json_encode([
'success' => true
]));
$this->response->send();
}
}
Note: Instead of using the StatusDetails node, it is advisable to use theEndpointType
node of the callback.
This controller (MVC, as mentioned above) responds to POST requests at the URL /api/collanapay
, as described earlier. It fulfills the two tasks mentioned above by parsing the HTTP request body. In the first step, it establishes a connection to the payment process being handled using the transactionId
. For the sake of clarity, error handling, logging, etc., are omitted.
In the followingswitch...case
- statement, the code handles the various possible responses from Collana Pay. Let's directly consider the first case, which involves processing the asynchronous response to the transaction creation. The backend already received the transactionId synchronously, and now Collana Pay asynchronously reports the creation of the transaction. The method processCreateInteractionResult() can now be implemented as follows:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processCreateInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
\FlenoCollanaPay\Api::PrepareTransaction($body['TransactionId'], [
'...' // payload für /transactions/{id}/prepare
]);
$payment->status = 'created';
$payment->action = '';
$this->container->get('models')->flush();
}
}
Here, it becomes evident that the backend fulfills both tasks: it locally persists the payment state and initiates the next step. The implementation of the ::PrepareTransaction()
method is very similar to the previously mentioned ::CreateTransaction
method and may look something like this:
namespace FlenoCollanaPay;
use GuzzleHttp\Client;
class Api
{
/**
*
* @param array $body
*/
public static function PrepareTransaction($transactionId, array $body)
{
$client = new Client();
$response = $client->post(sprintf('%sapi/v1/transactions/%s/prepares', self::getCollanaPayEndpoint(), $transactionId), [
'body' => json_encode(self::prepareBody($body), JSON_UNESCAPED_SLASHES),
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'ApiKey' => self::getApiKey()
]
]);
if($response->getStatusCode() !== 202)
{
throw new \Exception('invalid status code');
}
return json_decode($resp->getBody(), true);
}
}
Please note that this code snippet refers to the implementation of the class shown earlier. This is crucial, especially concerning the prepareBody
method, which ensures that each request to the collana pay API receives the known callback URL.
In summary, the following has happened up to this point:
- The asynchronous response to creating the transaction has been received.
- The information from it has been stored as the persistent state of the payment, identified by its transaction ID, in the shop.
- The next processing step has been initiated; the backend now expects the next asynchronous response from collana pay on the callback URL.
Now, let's consider the implementation for the next step, Collana Pay's response to the /prepares
-call:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processPrepareInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
$payment->status = 'prepare';
$payment->action = 'iframe';
$payment->redirectUrl = $body['EmbedmentUri'];
$this->container->get('models')->flush();
}
}
As known from the process diagram, collana pay responds to /prepares
with a URL that the frontend should display in an iframe. This information is written to the local state and immediately sent to the frontend via long polling. There, the customer will fill out the form, triggering another asynchronous call to the callback URL.
However, as we are still in the/prepares
-step, this call will again land in this method since the EndpointType of the request remains Prepares. Therefore, the method needs to be extended to handle both cases:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processPrepareInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
switch($body['StatusDetails'][1])
{
case 'Pending':
$payment->status = 'prepare.pending';
$payment->action = 'iframe';
$payment->redirectUrl = $body['EmbedmentUri'];
break;
case 'Successful':
\FlenoCollanaPay\Api::ReserveTransaction($body['TransactionId'], [
'...' // payload für /transactions/{id}/reserve
]);
$payment->status = 'prepare.done';
$payment->action = '';
$payment->redirectUrl = '';
break;
}
$this->container->get('models')->flush();
}
}
The implementation of ::ReserveTransaction()
follows the familiar pattern, so we won't delve into it here.
Next is the processing of the asynchronous response to it, also following a familiar pattern:
class Shopware_Controllers_Api_CollanaPay extends \Shopware_Controllers_Api_Rest
{
/**
* @var \FlenoCollanaPay\Models\Payment\Payment $payment
* @var array $body
*/
private function processReserveInteractionResult(\FlenoCollanaPay\Models\Payment\Payment $payment, $array $body)
{
switch($body['StatusDetails'][1])
{
case 'Pending':
$payment->status = 'reserve.pending';
$payment->action = 'redirect';
$payment->redirectUrl = $body['RedirectUri'];
break;
case 'Successful':
$payment->action = 'success';
$payment->status = 'reserve.done';
$payment->redirectUrl = null;
break;
}
$this->container->get('models')->flush();
}
}
Note: Here, instead of using the StatusDetails node, theStatus
node of the callback can be used.
This implementation highlights that each sub-step is directed back to the same controller. The flow logic is controlled by the switch-case
tatement responding to the EndpointType
he asynchronous response. Depending on the sub-step, the current status is passed to the payment
-model, ensuring that it is informed about the current status at all times.
Frontend
As mentioned earlier, the implementation described here relies on Long Polling to facilitate data exchange between the web browser and the web server. The following section will provide a closer look at the superiority of Long Polling over traditional polling. From a purely technical standpoint, the use of WebSockets would be even better; however, we choose to forego it in this case. In the e-commerce environment, it is necessary to ensure broad compatibility to serve as many customers as possible. With modern technologies like WebSockets, this is not always guaranteed, whereas Long Polling describes not a specific technology but rather a design pattern based on simple, established technologies. Even when using WebSockets, fallback solutions must be offered for older systems, which, in turn, will be based on the procedures described below. The use of libraries like socket.io is not considered here, as they also implement what is described here. Moreover, libraries are inherently generalized, which leads to two consequences that we do not want to assume here:
- An implementation tailored to one's own systems is often more performant.
- The encapsulation ensures that developers do not need to know the underlying workings, but this can be a disadvantage when problems arise.
The second point, in particular, contradicts the spirit of this guide, which aims to create a deep understanding of the functioning of Collana Pay and its integration. Moreover, this guide operates under the premise that existing online shop systems should be connected to Collana Pay. Many established systems often rely on older, well-developed, and widely used technologies. In traditional polling, a client sends a request to the server at regular intervals to retrieve messages. The server immediately responds, either with a message if one is available or with an empty response otherwise.
In Long Polling, a client also sends a request to the server to retrieve messages. If a short message is available for the client, the server responds immediately, just like in traditional polling. However, if no short message is available for the client, the server delays the response, keeping the connection open until a message is available or a certain time period elapses (timeout). Once the client receives a response, it sends another request to the server.
The server, in part, takes on the work of the client by implementing a loop and handling the regular updating of the request. The significant advantage here is that the overhead of constant TCP connections (establishment, termination, data transfer) is reduced.
Additionally, Long Polling can achieve a nearly real-time response feel, whereas with traditional polling, considering the resources of the client and server, there is almost always a noticeable delay.
Example
To illustrate the principle, here is a minimal implementation of long polling.
The minimal implementation of long polling shown here is based on jQuery. It queries new messages from the /messages route. If the response is empty, it is ignored. If an error occurs (e.g., timeout), it is also ignored. The use of always() ensures that, in any case, after the server response, regardless of its type, a new request is initiated. This ensures a continuous connection to the server.
document.asyncReady(function()
{
function longpolling() {
jQuery.ajax('/messages', {
method: 'GET',
dataType: 'json'
}).success(function(response) {
if(!response.messages.length)
return;
// verarbeite response.messages
}).always(function() {
longpolling();
});
};
longpolling();
});
In reality, two significant extensions are often defined:
- The invocation of
longpolling()
in the.always()
-Callback is often encapsultated in awindow.setTimeout()
which allows controlling the client's resource load. The error case is monitored with the.fail()
callback, too many errors in a short time should either interrupt repeated call or at least delay it (e.g., an extended timeout in 1.).
Server (PHP / Laravel)
The server-side implementation of long polling, based on the PHP framework Laravel. As soon as a new record is entered into the SQL table messages
it is returned by the server. As long as no entry is available, the server will periodically query the database.
A timeout is defined (here: 30 seconds), after which the connection is terminated. This is done to preempt any timeout from deeper layers, such as the operating system.
Route::get('messages', function ()
{
$timeout = 30;
$start = time();
do
{
$messages = DB::select('SELECT * FROM messages WHERE unread = 1');
if(!empty($messages))
{
DB::update('UPDATE messages SET unread = 0 WHERE unread = 1');
return response()->json([
'messages' => $messages
]);
}
} while($start + $timeout > time());
return response()->json([
'messages' => []
]);
});
This implementation is also for illustrative purposes and would need to be expanded in reality. Regardless of the very naive use of SQL, the following extensions are often made:
- Within the loop, a
sleep()
function is often defined to reduce or finely control server load. - In thread-capable languages, the loop is often moved to a separate thread to avoid bottlenecks in I/O streams.
Applying the procedure
Now that the principles of the process are clear, let's use them for the actual implementation of collana pay.
At a specific point in the shop checkout process, the payment via collana pay was initiated, as described in the previous "Backend" section.
We step in after the processing of the payment has been initiated by Collana Pay, meaning atransactionId
is available. The user has been directed to a page within the shop system where the processing by Collana Pay is now taking place.
The client now assumes two tasks:
- He serves as a trigger for the server to perform the next steps in the processing.
- It informs the customer about the current status of the payment processing.
So, we use the above-mentioned example and make a few adjustments:
<div id="collanapay">
<p id="status">Ihre Zahlung wird verarbeitet, bitte warten</p>
</div>
<script src="longpolling.js"></script>
/**
* führt das longpolling durch
*/
var status = null;
function longpolling()
{
jQuery.ajax('/collanapay/status', {
method: 'GET',
dataType: 'json',
data: {
transactionId: transactionId,
status: status
}
}).success(function(response) {
processResponse(response);
}).always(function() {
longpolling();
});
};
/**
* verarbeitet die Antwort des Servers
* @param response
*/
function processResponse(response) {
if(!response.update)
return;
status = response.payload.status.code;
processStatus(response.payload.status);
processAction(response.payload.action);
};
/**
* aktualisiert die Statusmeldung für den Nutzer
* @param status
*/
function processStatus(status) {};
/**
* führt Aktionen im Client aus, die vom Server angefordert werden
* @param action
*/
function processAction(action) {};
document.asyncReady(function()
{
longpolling();
});
In this simple implementation, some differences from the previous example code are noticeable.
Firstly, the current state is stored in the variablestatus
which is initially undefined. This information is communicated to the server during the connection setup, ensuring that the server is aware of the client's information state.
The implementation of longpolling()
delegates the processing to a separate method. Aside from improving clarity, this approach offers additional advantages. It allows room for error handling, although not implemented here, by encapsulating the call to processStatus
within atry...catch
-block. More importantly, it separates the process from transmission, meaning the implementation of alternative channels like Websockets becomes significantly simplified on this end, as previously mentioned.
Following only this example, we must assume that the server responds on the route /collanapay/status
with a message consisting of at least two attributes:
-
status
- An object that transmits the current state ascode
and in textual form within themessage
. -
action
- An object that can prompt the client for further activities.
Before we examine the implementation of the methods that handle these two attributes, let's first take a look at the server-side implementation. In the initial example, we showed code based on Laravel to quickly and easily illustrate the principle.
From now on, for demonstration purposes, we will be using (abbreviated) code from the implementation for the Shopware e-commerce system. This code is utilized within the familiar class
Shopware_Controllers_Frontend_CollanaPay
:
class Shopware_Controllers_Frontend_CollanaPay extends \Shopware_Controllers_Frontend_Payment
{
private $timeout = 30;
public function statusAction()
{
$transactionId = $this->request->getParam('transactionId');
if(!$transactionId)
{
return $this->response->setHttpResponseCode(400)->send();
}
try
{
$repository = $this->container->get('models')->getRepository(\FlenoCollanaPay\Models\Payment\Payment::class);
$payment = $repository->findOneBy($condition, [
'transactionId' => $transactionId
]);
if(!$payment)
{
return $this->response->setHttpResponseCode(404)->send();
}
$start = time();
$update = false;
$payload = null;
while($start + $this->timeout > time())
{
$queryBuilder = $this->createQueryBuilder('payment')->setCacheable(false);
$queryBuilder->where('payment.transactionId = ?1');
$queryBuilder->setParameter(1, $transactionId)
if($status = $this->request->getParam('status'))
{
$queryBuilder->where('payment.status != ?2');
$queryBuilder->setParameter(2, $status);
}
$payment = $queryBuilder->getQuery()->getOneOrNullResult();
if($payment)
{
$payload = [
'status' => [
'code' => $payment->status,
'message' => $payment->status
],
'action' => [
'code' => $payment->action,
'payload' => $payment->redirectUrl
]
];
$update = true;
break;
}
}
return $this->response->setBody(json_encode([
'update' => $update,
'payload' => $payload
]));
}
catch(\Exception $e)
{
return $this->response->setHttpResponseCode(500)->setBody(json_encode([
'message' => $e->getMessage()
]));
}
}
}
In this code, the transactionId
is initially checked, and an invalid ID triggers an HTTP error 400
. For the sake of clarity, the validation here is rudimentary, and it is advisable to implement additional formal checks for production use.
If no status code is provided, the server responds with the current state of the payment. Otherwise, the server responds when a state is reached that does not match the one known to the client. Therefore, if the client is aware of the statecreate.done
, the server will respond as soon as the state is no longer create.done
.
As seen here, the server responds with the current state of the payment processing in a format understandable by the frontend. The specific data was already provided by the controller handling collana pay's callback URL in the previous chapter. Once again, thetransactionId
serves as the coonecting element.
Now that we know what data is transmitted, let's turn our attention back to the frontend and the processing of the data.
/**
* aktualisiert die Statusmeldung für den Nutzer
* @param status
*/
function processStatus(status) {
var $p = $('p#status');
$p.html(status.message);
};
The implementation of processStatus()
in this example is remarkably simple and self-explanatory. The current state of the payment processing is presented to the customer in plain text.
A bit more complex is the processing of theaction
, ah shown in the following:
/**
* führt Aktionen im Client aus, die vom Server angefordert werden
* @param action
*/
function processAction(action) {
switch(action.code) {
case 'iframe':
document.location.href = action.payload;
var $iframe = $('<iframe />');
$iframe.attr('src', action.payload);
$('div#collanapay').append($iframe);
break;
case 'redirect':
document.location.href = action.payload;
break;
}
}
document.asyncReady(function()
{
longpolling();
});
As known from the flowchart, there are two actions that the frontend needs to execute. The rudimentary code for both tasks is presented here:
Attachment
Certainly! Feel free to provide the additional information you'd like to include, and I'll do my best to assist or provide relevant insights.
Scalability of Front-End Web Servers
Often, a web server, such as a reverse proxy, precedes the application server. In this scenario, it quickly becomes a critical resource for the long-polling application. Using commonly employed solutions like Apache and Nginx, let's take a closer look at potential issues and possible solutions.
Apache
The Apache HTTP Server provides various Multi-Processing Modules (MPMs) to handle requests:
- In the Prefork MPM, multiple processes are launched, each possessing a single thread to handle an individual request.
- In contrast, the Worker MPM utilizes multiple threads within a single process, with each thread responsible for processing an individual request.
For both MPMs, the number of concurrent Long-Polling requests is limited by the available threads. The default maximum is set to 256 for Prefork and 300 for Worker. These MPMs may not be well-suited for significantly more simultaneous Long-Polling requests, such as for handling 10,000 concurrently active requests. Since version 2.4, the Apache HTTP Server introduces an Event MPM designed to scale better than the two mentioned MPMs.
nginx
In contrast, Nginx fundamentally relies on an event-driven approach to handle requests, enabling the processing of 10,000 concurrently active requests by a single thread. The distinct behavior of Nginx compared to the Prefork/Worker MPM of the Apache HTTP Server becomes apparent at around 400 concurrently active Long-Polling requests. With 400 requests, the number exceeds the available threads in the default configuration of the Apache HTTP Server, leading to significant delays or even rejection of some requests. Meanwhile, Nginx's default single thread can efficiently forward all 400 Long-Polling requests directly to the application server.
Schlussfolgerung
For Long Polling with high user numbers, an event-driven processing approach by the web server is recommended. The choice of a processing mechanism needs to be made during the setup time of the web server. For the Apache HTTP Server, the MPM needs to be selected before compiling the Apache HTTP Server. In the case of Nginx, the installation of Nginx itself implies a decision for event-driven processing.
Longpolling
For Long Polling with high user numbers, an event-driven processing approach by the web server is recommended. The choice of a processing mechanism needs to be made during the setup time of the web server. For the Apache HTTP Server, the MPM needs to be selected before compiling the Apache HTTP Server. In the case of Nginx, the installation of Nginx itself implies a decision for event-driven processing.
Error handling, client side
The previously described implementation of Long Polling does not include client-side error handling. In client-side implementation, the focus is primarily on handling server resources gently. This is justified by the fact that with a sufficiently large number of active clients and a vulnerable server-side implementation, the server can quickly be burdened with an overwhelming number of requests. Common strategies aim to minimize unnecessary requests. If the server responds with an error message, a primitive implementation reacts with an immediate retry. If the problem is systemic, the server will keep generating the error message repeatedly, causing continuous stress on the client. The goal should be to recognize this scenario and respond accordingly.
A possible implementation can be structured as follows:
document.asyncReady(function()
{
var longpollingOptions = {
stats: {
success: 0,
fail: 0
},
sleep: 0,
onFail: function() {
if(prompt('Es gibt Probleme. Möchten Sie dennoch warten?')) {
longpollingOptions.stats.success = 0;
longpollingOptions.stats.fail = 0;
longpollingOptions.sleep = 100;
}
}
};
function longpolling() {
jQuery.ajax('/messages', {
method: 'GET',
dataType: 'json'
}).success(function(response) {
longpollingOptions.stats.success++;
longpollingOptions.sleep = 0;
if(!response.messages.length)
return;
// verarbeite response.messages
}).fail(function() {
longpollingOptions.stats.fail++;
longpollingOptions.sleep += 10;
}).always(function() {
if(longpollingOptions.stats.success < longpollingOptions.stats.fail) {
return longpolling.onFail();
}
window.setTimeout(longpolling, longpollingOptions.sleep);
});
};
longpolling();
});
This implementation pursues two strategies simultaneously:
Firstly, in case of an error, the time until the next retry is increased (longpollingOptions.sleep
).
This way, a client automatically becomes more resource-efficient over time. However, this comes at the expense of comfort and should be communicated. Secondly, this implementation essentially keeps track of successful and unsuccessful connections. If the number of errors increases and surpasses a certain threshold (besides the demonstrated comparison, absolute numbers, or percentage shares of all requests could be considered), the Long Polling application can be aborted.
Final Notes
This guide was collaboratively authored by collana hive GmbH and Fleno GmbH. It serves as an example and framework for a potential integration into e-commerce systems. The specific integration should always be customized and optimized based on the particular situation and the e-commerce system in use.
collana hive GmbH
Borselstraße 20
22765 Hamburg
support@collanapay.com
Fleno GmbH
Marie-Curie-Ring 31
24941 Flensburg
kontakt@fleno-gmbh.de