Tech Blog

Managing online payments with Symfony and PayFlow Link

By Ginger Bidwell, Clint Osborne and Mike Hagedon  (University of Arizona)

Our payments application allows our customers to pay for technology services and fines online through Alma.

Moving from Millennium to Alma

Originally we used Millennium for our library catalog. It allowed customers to pay for their late or lost fines. By setting up special user accounts, we were also able to use its system for community member payments for services such as 3D printing.

When we started our migration to Alma, we realized that there was not a service available in our region (US) for online payments. We had an in-person location where customers could pay, but it was becoming cost-prohibitive. Students could manage payments through the central bursar’s office, but it would be inconvenient. We also serve community users who are not affiliated with the library or the university.

Our requirements included the following:

  • Anyone with a library account can review and pay all or part of their fines
  • Credit card processing should happen in Payflow Link (the established solution on our campus)
  • Paid fines will be correctly removed from the user’s library account
  • Staff can reconcile the transactions using the same (or better) process than the previous Millennium system
  • Our solution must be PCI compliant

When we talked with developers at peer institutions also using Alma, we found out that they were managing fines and fees through the central financial office. The solution we were starting to explore seemed unique. We discussed possible technical solutions and decided to build a custom application to act as a bridge connecting the Alma API and Payflow Link.

These factors in our solution are unique to our institution’s environment:

  • Our campus is using legacy Payflow Link. During this project, we couldn’t investigate other payment processors.
  • We provide paid services to the community, so we need to accept payments from users whether or not they’re affiliated with the institution.

Our application structure

Symfony Framework

We used Symfony, a PHP framework, to build our application. A framework such as Symfony provides a prescribed structure for the application, in this case the Model View Controller (MVC) design pattern, along with a collection of libraries out of the box that make bootstrapping a full stack application such as this much easier.

Our application essentially serves as a bridge between the Alma API and Payflow Link. Out of necessity we developed this application using Payflow Link Legacy. Ideally we would be using the more up to date Paypal integration, Payflow Gateway. Payflow Link Legacy is considered deprecated by Paypal and Payflow Gateway comes with a broader set of functionality. With that being said, we’ll discuss here how we developed our application using the Payflow Link Legacy integration.

Components of our application that connect the Alma API and Payflow Link

One major component of our application is authentication. While this is important, an entire article could be dedicated to how we implemented authentication. Since this article is focused on the Alma API and Payflow connection, we focus on the workflow after the user has Authenticated. In general though we used Symfony’s Security Component to implement authentication in our application.

Service Classes

Before we jump into the MVC aspects of the application there are a couple objects that are important to mention. These objects are what Symfony refers to as Services. These are essentially utility classes that help facilitate some action, or set of actions, in our application. Our application has two Service classes that help facilitate the connection to the Alma API.

AlmaApi Service class

The AlmaApi class is essentially a wrapper for the Guzzle HTTP request library. This is where all of our Alma API requests are constructed and executed. What’s returned is the raw response data coming from the Alma API. This is made up entirely of class methods that make requests to the Alma User API. The actions that can be performed using this class include listing fees, paying fees, and authenticating users.

AlmaUserData Service class

Calls to the methods in the AlmaApi class are going to return the body of the Guzzle response. The AlmaUserData class takes in this response and returns it in a format that makes most sense for the destination location. In other words methods in this class will return an Array or a String of user data.

MVC components

Now we get to the components that truly make up the application. These are the components that make up the workflow for users viewing their fees and then paying them.

Listing fees

The first step is a user gets a list of their fees from Alma. This is done through the ListFeesController class. This controller is attached to the index route of our application. Using the AlmaUserData Service class, a list of fees for the current user is displayed. Each fee can be check marked for selection, to be paid, by the user.

For the Model part of our application we are using Symfony Entity Classes. These classes are attached to tables in our MySQL databases. We are using the Doctrine library to connect the Entity Class objects to the MySQL database. The two Entities in our application are Fees and Transactions. Transactions can have any number of Fee objects attached to them (One-to-many relationship).

Creating a Transaction instance

When a user selects the fees they want to pay and selects the “Pay now” submission button, they are directed to the ‘/pay’ route of the application. This triggers the PayController. It is here where a Transaction object is initialized. The Transaction Object is initialized based on the fees they have selected. Each fee in the Transaction object is initialized as a Fee object and creates an entry for each fee in the ‘fee’ table in the database. Each Transaction is also given a unique invoice_number.

Here is a snippet of the code for the PayController where all this is happening:

$transaction = new Transaction($this->getUser()->getUsername());

$entityManager = $this->getDoctrine()->getManager();
if ($this->setUserFees($transaction, $feeIds) == 0) {
    return $this->redirectToRoute('index');
}

$entityManager->persist($transaction);
$entityManager->flush();

return $this->render('views/pay.html.twig', [
    'user_id' => $transaction->getUserId(),
    'invoice_number' => $transaction->getInvoiceNumber(),
    'total_balance' => $transaction->getTotalBalance(),
    'payflow_url' => getEnv("PAYFLOW_URL"),
    'payflow_login' => getEnv("PAYFLOW_LOGIN"),
    'payflow_partner' => getEnv("PAYFLOW_PARTNER"),
]);

Sending Transaction data to Payflow Link

All the Transaction data is passed to the template that the user is presented with at the ‘/pay’ route that they are directed to after selecting the fees they wish to pay. The user is asked to verify the total amount to be paid. The payment confirmation page is made up of a form with hidden input values that are passed to Payflow. Here’s a snippet of the pay.html.twig template to give you an idea of what that looks like.

<div class="confirmation__wrapper">
    <div class="confirmation__inner">
        <h2>You're about to pay ${{ total_balance|number_format(2, '.', ',') }}</h2>
        <form action="{{ payflow_url }}" method="post">
            <input type="hidden" name="LOGIN" value="{{ payflow_login }}">
            <input type="hidden" name="PARTNER" value="{{ payflow_partner }}">
            <input type="hidden" name="AMOUNT" value="{{ total_balance }}">
            <input type="hidden" name="TYPE" value="S">
            <input type="hidden" name="CUSTID" value="{{ user_id }}">
            <input type="hidden" name="INVOICE" value="{{ invoice_number }}">
            <a class="button button-link" href="{{ path('index') }}">Cancel</a>
            <input id="pay_form" type="submit" value="Continue" class="button button-primary">
        </form>
    </div>
</div>

The user only sees the message asking them to verify the total amount to be paid along with a “Continue” submission button. That submission button submits the form to Payflow Link. The hidden values are properties that Payflow Link is expecting on their end. The user is then directed to Payflow Link (https://payflowlink.paypal.com/) where they will input their credit card information.

Receiving Transaction data back from Payflow Link

Throughout this process, the Transaction that the user has created in our payment application has a ‘status’ of PENDING. This is reflected in the Transaction data that is persisted in our database. Once the user has gone through credit card processing on the Payflow Link side of things, they are redirected back to our application at the ‘/result’ route. This executes our ResultController. This does not render any Twig template. The controller strictly serves as a place for processing the Silent Post request from Payflow Link.

Payflow Link will send back a number of values including the result of the transaction (e.g. COMPLETED, DECLINED, etc). Payflow Link will also send back the invoice number of the transaction. This is how our Payments application retrieves the users Transaction from the database. If Payflow sends the status back indicating a successful Transaction, the ResultController will update the users fees in Alma using the Users API for Alma, and the Transaction is updated in our database.

Portfolio considerations

Building and maintaining a custom application for this use case has portfolio implications for the team and organization supporting it. This solution fit our organization well because we have several developers and platform engineers creating and maintaining software and infrastructure in-house.

Beyond creating the software itself, a lot of work goes into maintaining PCI compliance. Multiple people in the organization are involved in communication and training. We run security scanning software regularly, and each year we submit documentation about the technical setup to validate that we are in compliance. When we were using Millennium, the vendor handled the PCI compliance work.

We also upgrade the Symfony framework when each new version is released. This adds regular maintenance tasks onto the portfolio of software we already maintain. 

Conclusion

During our move from Millennium to Alma, we created our custom payments application to act as a bridge between Alma and the payment processor. We used the Symfony framework to provide the structure for the application, and added classes for working with data from the Alma API and creating and managing transactions. This application helped our organization move to Alma without losing the ability for customers to pay online. Online payments are more convenient for customers than going through a central campus office and more cost-effective for the organization than maintaining an in-person register. 

2 Replies to “Managing online payments with Symfony and PayFlow Link”

Leave a Reply