ExSite can support any payment processor, in principle. The E-commerce checkout process includes hooks for calling into a payment processor's API to perform the necessary work at each point in the process. To add support for a new payment processor, you need to provide the bridge code that responds to those hooks, and translates them into appropriate calls to the payment processor API. This bridge is called a Payment Gateway Plug-in.
Your bridge code will go into a special module in /cgi/Modules/Pay/
. The module should be named for the payment API that it services. For example, supported payment plugins include
There is also a Test.pm
module in that same directory that provides a functional test payment module that will process fake payments in a testing and development environment. It can be also used as an example implementation.
When the checkout process reaches the payment step, the e-commerce system will talk to your payment gateway module through a particular sequence of calls:
1. |
IsReceipt() |
|
NO | YES | |
2. |
SetupTransaction() |
Success() |
3. |
PostTransaction() |
Cancel() |
The payment flow happens in two parts, corresponding to the left and right side of this table. The first/left part prepares and executes the payment transaction. The second/right part determines whether or not it succeeded. The IsReceipt() call determines which side of this flowchart we use.
The pre-payment flow (left side) generally ends with the customer at a credit card input form. The post-payment flow (right side) generally ends at a receipt, although if the payment failed they could also be returned to their shopping cart to try again.
In addition to these payment flow calls, the bridge code needs to support a number of calls to report information on the status of the payment transaction. There are also several optional hooks for more advanced functions like refunds, automated payments, and subscriptions. Simple purchases don't make use of those; only the above hooks are required to process a normal e-commerce purchase.
Your payment bridge module should support the following calls. These methods are all optional; however if you do not define one, that call will effectively return FALSE, which could have implications for the payment flow.
A number of the calls below return TRUE/FALSE (eg. 1/0) values to indicate success or failure. In the event of failure, the bridge code should set an internal status to indicate the nature of the problem. The Status() method should return this status message to the e-commerce system to report on payment issues.
Setup(%opt)
This is always called when a payment bridge is instantiated. It can perform any preconfigurations that are needed for the rest of the code to run. It receives all the same parameters as the SetupTransaction() call, below.
The Setup call should return TRUE/FALSE for success/failure. In the event of failure, the payment subsystem will not attempt to process any transactions.
IsReceipt()
This is is the first method called, to determine where we are in the payment flow. If true (1), the payment flow has completed, and we are simply reporting the outcome. Otherwise, the payment transaction has not happened yet, and we need to prepare a new transaction.
With many payment processors, there is a receipt page callback that occurs when a payment is completed. This callback will include parameters in the query string or in the POST data that communicate the status of the transaction. (These may or may not need to be verified with additional API calls.) But the presence of such parameters is usually sufficient to recognize that we are on the receipt side of the transaction.
SetupTransaction(%opt)
This call prepares a payment transaction. Not every payment processor will require a setup step that is distinct from the post step (next). But it is typical of payment APIs where you must first obtain transaction tokens before you can post a payment.
The passed options will include:
The SetupTransaction call should return TRUE if the transaction is successfully prepared, and FALSE otherwise.
PostTransaction()
This call sends the prepared transaction to the payment gateway. For hosted payment forms, this will send the customer to the hosted pay page. For integrated payment forms, it will generate the payment form for insertion into the current web page. At this point the customer will need to interact with the payment form to provide their payment information and/or approval.
The PostTransaction call should return TRUE if the customer was directed to a payment form successfully, or FALSE otherwise.
Success()
If we are on the receipt side of the payment flow, the first thing we do is call the Success method to find out of the payment succeeded. This should return TRUE (1) if the payment was completed successfully, and FALSE (0) if not.
If the payment was successful, you can use Pass() to provide an appropriate success message from the payment provider.
Cancel()
Returns TRUE (1) if the customer cancelled the payment transaction at the payment form. We check for this in the case that Success() returned FALSE. Note that a cancelled payment is NOT a cancelled purchase; the customer can re-try payment if they like.
If the payment was not successful, and the customer did not cancel, then the transaction failed for some other reason. You can use Fail() to provide an appropriate error message from the payment provider. When assessing the outcome of a payment transaction, the control flow is:
if (Success) {
... payment was made
}
elsif (Cancel) {
... payment was canceled by user
}
else {
... payment failed
}
Status messages describing the outcome of the payment transaction can be returned to the e-commerce system via the following methods:
Status()
Returns a status message for the most recent call to the bridge module. Use this to report any problems with preparing or posting payment transactions.
Pass()
Called following a successful transaction. Any special payment gateway code that needs to run after a payment is successfully completed can go here.
The Pass
method should respond with a message describing the successful transaction. This will typically just be the standard transaction succeeded message from the payment provider. This message is shown to the customer, and included in payment notes.
Fail()
Called following a failed transaction. Any special payment gateway code that needs to run after a payment fails can go here. (Note that a canceled payment is not considered a failure, and will not invoke the Fail
method; use the Cancel
method instead.)
The Fail
method should respond with a message explaining the failure. These can be much more varied, depending on the status of the card and the security settings on the merchant account. This message will be displayed to the customer, and also in the payment notes when the failed payment is recorded.
More specific details of the transaction outcome can be communicated to the e-commerce system via the following methods:
GetTransactionId()
Returns the unique transaction ID for the transaction, as assigned by the payment processor.
GetPurchaseId()
Returns the original invoice/receipt # for the purchase.
GetPurchaserId()
Returns a unique purchaser ID for the customer (note the extra r), as assigned by the payment processor.
This is an optional feature. We already have our own unique purchaser ID (the account ID), but some payment processors may maintain their own separate customer profiles, where they can save purchase preferences, contact info, saved cards, and so on. This method allows you to retrieve that payment processor customer ID in case you need it for other advanced features such as automatic billing. If a value is returned, it will be stored in the pgcode
column of the account
table.
GetAmount()
Returns the amount received in a payment transaction. This should be the same as the amount you set the transaction up for, but it can be different in some cases (for example payment plans).
GetPaymentID()
Returns some descriptive information about the payment method, such as the credit card type, or the last 4 digits of the card. This is an optional feature that allows you to capture additional metadata about the payment, if the payment processor allows for it. This information will automatically be stored in the payment record, and will be available in financial reports as a payment identifier.
The bridge module can optionally support the following advanced payment functions.
Config()
The Config()
method allows you to operate a customer payment configuration page. This is useful for managing payment preferences, such as tokenized cards. This method generates the HTML for those configuration screens, as well as processing any inputs to those screens.
GetReceiptPageId()
Most payment processors let you configure your receipt URLs, where the customer gets redirected after a successful payment. Sometimes a single URL is not sufficient, however. For example, multilingual sites may want to redirect you to different receipt pages depending on the preferred language of the customer. If you need to support more receipt pages than your payment processor will accommodate, you can use this method to select a different one than the payment processor directed you to.
PostRefund(%opt)
Process a refund automatically. The passed options are:
This will refund the payment back to the customer. More information is given below, under Automatic Refunds.
This method should return TRUE/FALSE (1/0) to indicate success/failure of the refund transaction. In the event of a failure, the Status
method should return a message explaining the problem.
PostPayment(%opt)
Process a single payment automatically. Regular e-commerce transactions occur when the customer is present and able to enter their card details into the payment form. But if your payment processor allows it, customers can leave their card information in their purchaser profile, and you can make use of that to request automated payments when the customer is not present.
The passed options are:
This method should return TRUE/FALSE (1/0) to indicate success/failure of the payment transaction. In the event of a failure, the Status method should return a message explaining the problem.
More information on how automated payments are set up and scheduled is given below, under Automatic Payments.
MultiPay(%opt)
Process a string of automatic payments. These are typically part of a payment plan or subscription that requires making scheduled payments at specific intervals; for example, splitting an annual membership fee into quarterly payments.
To set up a payment plan, you need to pass the following parameters to MultiPay():
More information on how payment plans are set up and scheduled is given below, under Automated Payment Plans.
Automatic refunds occur when the e-commerce tools can actually return money directly to the customer's card. If not supported, the e-commerce tools can only record the refund in the accounting system; the actual transaction to return the funds will need to be done manually through your merchant terminal.
To keep things simple and orderly, automatic refunds will only reverse whole payments. Some payment processors will allow refunds or chargebacks that don't match any previous amount charged. The accounting scenarios there are potentially quite complicated and messy, so if you need to refund a partial or irregular amount, you should do it manually to ensure the accounting is correct.
To perform an automatic refund, go to your invoice in the Payment module, choose the cancellation/refund tool, and select the Automatic Refund tab.
In your payment gateway module, your PostRefund
method will process refund requests done via that tab. It receives a payment record and a purchase ID. It should refund that payment amount in full. Note that the original transaction ID can be found in the txid
field of the payment record, and the payment processor's customer profile ID can be found in the pgcode
field of the account record.
If your payment provider does not support automatic refunds, simply don't define a PostRefund
method in your payment gateway module.
There are two methods to support automatic payments:
The first case is handled by payment records with a "pending" status, and a future payment date. If the payment is for a specific invoice, that invoice should also be configured with a "pending" status and similar date. The invoice will automatically be enabled when the payment is processed.
If your site supports scheduling automatic payments, you need to set up a scheduled task to process those pending payments. In the Task Manager, create a task with the following settings:
Every night, this task will find pending invoices that have come due and attempt to automatically process them. The pending payment record (and its associated invoice ID) will be passed to your payment gateway's PostPayment
method for processing.
There are three configuration settings that determine how failed automatic payments are handled:
Pay.reschedule_missed_payments
: if true, the automatic payment tools will mark the payment as failed, but will add a new scheduled payment to re-try.Pay.reschedule_interval
: this sets the number of days between re-tries. For example, if set to 7, the next scheduled payment will be attempted 7 days after the failure.Pay.max_missed_payments
: this sets how many payments will be attempted before the automatic payment subsystem gives up.E-mail notifications are sent to the customer when an automated payment fails. This is often because cards have expired since the payment was first set up, and it gives the customer a chance to update their card info at the payment processor, or to log in and make the payment manually.
A typical use of this feature is automatic renewal of a membership or subscription when the current term expires.
Multipay
is best used for payment plans, that is for single purchases that are paid in multiple instalments. An example would be a yearly membership fee that is broken down into quarterly payments. This style of paying is not allowed on all purchases, so you have to explicitly request a payment plan in the code that adds the purchase to the cart. To do this, the code should set a payment_plan_request
in the customer's session. It is a hashref that can hold the following values:
The shopping cart does not need to honor the payment plan request when the customer checks out. The request will be ignored if the purchase ID or purchased object do not match the request. It may also be ignored if the invoice total has changed, indicating that other purchases have been added to the cart, which may not be eligible for payment plans.
If the payment plan request is accepted, the payment plan request information will be included in the parameters that are passed to SetupTransaction. That method should proceed with a regular e-commerce payment for the first instalment only. This is normally the total divided by the number of instalments, plus any residual amount needed to round off the total.
For example, if a $100 invoice is to be paid in 3 instalments, the individual payments will be
$100 ÷ 3 = $33.33
But the first payment should be rounded up to $33.34 so that the payments will total $100. Otherwise you will only collect $33.33 × 3 = $99.99, leaving you with an annoying 1¢ accounting imbalance.
It is customary for the payment_plan_request
in the session to be updated to a payment_plan
in the session, so that any code that needs to know will be able to understand that the payment rules for this purchase are different. The payment_plan
is a session variable is similar to the payment_plan_request
, except that it reflects that actual values being used, rather than the suggested values that were requested.
The customer will proceed through a normal online payment for the first instalment amount. If the payment succeeds, the rest of the payment plan can be scheduled. The Pass
method can be used to initiate the remainder of the scheduled payments, since it only gets called if the first instalment succeeds. It will have access to the payment_plan
info in the session.
It is expected that the remaining payments in the payment plan will be scheduled using appropriate features in the payment processor's API. However, it is technically possible for Multipay
to schedule the remaining payments using regular scheduled payments, as above.
Note that when the payment plan completes, you will have multiple successful payments on a single invoice. The purchase processing (purchase activation) happens after the first payment goes through, even though payment is still incomplete. Receipts will show the full invoice, but the current payment status will be noted in the payment information section of the receipt; this will say something like "Paid $X of $Y" to indicate that payment is ongoing and incomplete.
When a payment succeeds, the payment processor will typically initiate some sort of call-back to the originating website. This is normally what generates the receipt view of the purchase.
Every payment processor does this differently. Some will return you to your website proper and allow you to generate the receipt yourself. In this case, you should automatically be connected to your original session.
Other payment processors will make their own back-channel connection to your website that the customer cannot see. This allows for better security, since the purchaser cannot tamper with this part of the process. However, that back-channel connection is not coming from your customer, so it will be lacking the session cookie that allows you to track the purchase details. That will prevent you from cleaning up the session to indicate that the purchase is done, which can result in undesirable behaviours like the cart still thinking that the purchase is underway.
So you need a way to communicate the original session ID to the back-channel connection, so that it can find the original session without a session cookie. Here are some methods to consider:
$token{'MyPaymentGateway-INVOICE-NUMBER'}
.In either case, once you have determined the session ID that the original purchase occurred under, you can hijack that session using
# disable security checks that prevent session hijacks
$ENV{BYPASS_SECURE_SESSIONS} = 1;
# ignore the default session that was started for the back-channel connection
(tied %session)->set("dirty",0); # don't save tmp/dummy session
untie %session; # disconnect
# connect to the original session
tie %session, 'ExSite::Session', $ORIGINAL_SESSION_ID;
Then you can proceed as if you were the original customer. (Note: you do not need to clean up the session yourself; the shopping cart system will do this automatically. You only need to ensure that you are connected to the right session.)