External Payment Types

Tags: v6

General Idea

By implementing the interface IExternalPaymentProcessor and registering it accordingly, a new payment system will appear in SyrveRMS. This can be simply referred to as an external payment type. For plugins that implement external payment types, special licensing is introduced.

Registering an External Payment System

The plugin registers the payment system using IOperationService.RegisterPaymentSystem(...). The mandatory parameter paymentSystem – an instance of the class implementing IExternalPaymentProcessor – is passed to this method. As a result of registration, a new payment system will appear in Syrve Office. It can be seen in the payment types under the “External Payment Type” section with the name IExternalPaymentProcessor.PaymentSystemName.

backPT

If a payment type for this payment system is created, the option to select this external payment type will appear on SyrveFront at the checkout and prepayment screens.

frontPT

Explanations of terms:

Interface IExternalPaymentProcessor

To implement the necessary business logic for processing and refunding payments with an external payment type, you need to implement the interface IExternalPaymentProcessor:

public interface IExternalPaymentProcessor
{
    string PaymentSystemKey { get; }
    string PaymentSystemName { get; }

    void CollectData(Guid orderId, Guid paymentTypeId, [NotNull] IUser cashier, IReceiptPrinter printer, UI.IViewManager viewManager, IPaymentDataContext context, UI.IProgressBar progressBar);
    void OnPaymentAdded([NotNull] IOrder order, [NotNull] IPaymentItem paymentItem, [NotNull] IUser cashier, [NotNull] IOperationService operationService, IReceiptPrinter printer, UI.IViewManager viewManager, IPaymentDataContext context, UI.IProgressBar progressBar);
    bool OnPreliminaryPaymentEditing([NotNull] IOrder order, [NotNull] IPaymentItem paymentItem, [NotNull] IUser cashier, [NotNull] IOperationService operationService, IReceiptPrinter printer, UI.IViewManager viewManager, IPaymentDataContext context, UI.IProgressBar progressBar);

    void Pay(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar);
    void EmergencyCancelPayment(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar);
    void ReturnPayment(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar);
    void ReturnPaymentWithoutOrder(decimal sum, Guid paymentTypeId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IProgressBar progressBar);

    void PaySilently(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IPaymentDataContext context);
    void EmergencyCancelPaymentSilently(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IPaymentDataContext context);
    bool CanPaySilently(decimal sum, Guid? orderId, Guid paymentTypeId, IPaymentDataContext context);
}

Here:

Payment Processing Method

When a user on SyrveFront selects a payment type on the checkout screen, specifies the amount, and clicks the “Pay” button, or when the user makes a prepayment with a specific payment type, control will go to the method Pay():

void Pay(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar);

Here:

More details about the object signatures can be found in the documentation.

For example, if integration with a hotel system needs to be implemented:

[Serializable]
internal class IsCardClass
{
    public bool IsCard;
}
public void Pay(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, IPointOfSale pointOfSale,  IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar)
{
    // Show a dialog in SyrveFront for entering the number and swiping the card
    var input = viewManager.ShowInputDialog("Enter number or swipe card", InputDialogTypes.Card | InputDialogTypes.Number);
    string room = null;
    string cardTrack = null;

    // If a number was entered, the result is of type NumberInputDialogResult
    var roomNum = input as NumberInputDialogResult;
    if (roomNum != null)
        room = roomNum.Number.ToString();

    // If a card was swiped, the result is of type CardInputDialogResult
    var card = input as CardInputDialogResult;
    if (card != null)
        cardTrack = card.FullCardTrack;

    if (room == null && cardTrack == null)
        // Nothing was entered, aborting the operation.
        throw new PaymentActionFailedException("No data was entered.");

    // Get the order using the API by id through IOperationService.
    var order = PluginContext.Operations.TryGetOrderById(orderId.Value);

    // Execute arbitrary methods. For example, process payment in some hotelSystem, which will return the guest's name if the payment is "accepted" and null if the payment is declined.
    var guestName = hotelSystem.ProcessPaymentOnGuest(cardTrack, room, order?.Number, transactionId, sum);
    if (guestName == null)
        // Payment failed, aborting the operation.
        throw new PaymentActionFailedException("Payment failed.");

    // Creating a receipt for printing. The receipt consists of XElement
    var slip = new ReceiptSlip
    {
        Doc =  new XElement(Tags.Doc,
            new XElement(Tags.Pair, "Guest", guestName),
            new XElement(Tags.Pair, "Amount", sum))
    };

    // Printing.
    printer.Print(slip);
    var cardInfoData = new IsCardClass { IsCard = card != null };
    var cardType = cardInfoData.IsCard
        ? "My Hotel System Card"
        : "My Hotel System Room";
    // Saving data that will be shown in reports.
    context.SetInfoForReports(room ?? cardTrack, cardType);
    // Saving data that will be used for refunding the payment.
    context.SetRollbackData(cardInfoData);
}

The exception type PaymentActionFailedException is used to abort the payment operation. The user on SyrveFront will be shown the message of this exception. This makes sense if there are any issues communicating with the external service, the payment cannot be processed, and the user needs to be informed of the reasons.

For “silent” cancellation of the operation, you can use the exception type PaymentActionCancelledException. This makes sense if a dialog window was shown during the payment process and the user clicked the “Cancel” button.

The arguments IReceiptPrinter, IViewManager, and IPaymentDataContext “live” only during the execution of the method; after the method completes, the instances are destroyed. So it does not make sense to store them in variables, as they cannot be used outside the method.

Silent Payment Processing

Sometimes businesses need solutions for payment plugin types from the plugins themselves, without entering the SyrveFront checkout screen. To do this, the plugin must implement the method CanPaySilently of the payment processor plugin. The result of the method is an answer to the question “Does the plugin have the ability to process payments silently?”. For this capability to exist, a plugin payment item must be added to the order in advance. For silent payment processing, you can call the method ProcessPrepay with the flag isProcessed set to false. The SDK provides an example using a custom class with the property SilentPay:

[Serializable]
public class PaymentAdditionalData
{
    public bool SilentPay { get; set; }
}

private string Serialize<T>(T data) where T : class
{
    using (var sw = new StringWriter())
    using (var writer = XmlWriter.Create(sw))
    {
        new XmlSerializer(typeof(T)).Serialize(writer, data);
        return sw.ToString();
    }
}
private void AddAndProcessExternalPrepay()
{
    var order = PluginContext.Operations.GetOrders().Last(o => o.Status == OrderStatus.New);
    var paymentType = PluginContext.Operations.GetPaymentTypes().Single(i => i.Kind == PaymentTypeKind.External && i.Name == "SamplePaymentType");

    var additionalData = new ExternalPaymentItemAdditionalData
    {
        CustomData = Serialize(new PaymentAdditionalData {SilentPay = true})
    };
    var credentials = PluginContext.Operations.AuthenticateByPin("777");
    var paymentItem = PluginContext.Operations.AddExternalPaymentItem(order.ResultSum, false, additionalData, paymentType, order, credentials);

    PluginContext.Operations.ProcessPrepay(credentials, order, paymentItem);
}

In turn, SyrveFront passes the specified serialized class for payment into the payment context (IPaymentContext), and then in the method CanPaySilently, the class is extracted and deserialized:

public bool CanPaySilently(decimal sum, Guid? orderId, Guid paymentTypeId, IPaymentDataContext context)
{
    var customData = context.GetCustomData<PaymentAdditionalData>();
    return customData?.SilentPay ?? false;
}

Depending on the return value of the method CanPaySilently, SyrveFront will call either the Pay or PaySilently method of the plugin processor. Thus, the plugin itself decides how the newly added payment should be processed.

Payment Return Methods

void EmergencyCancelPayment(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar);
void ReturnPayment(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar);
void ReturnPaymentWithoutOrder(decimal sum, Guid paymentTypeId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IProgressBar progressBar);
void EmergencyCancelPaymentSilently(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IPaymentDataContext context);

The methods EmergencyCancelPayment() and ReturnPayment() are called when the user initiates a return of a previously completed payment on SyrveFront.

Control is passed to the ReturnPayment() method when the “Partial Receipt Return” or “Delete Order” button is pressed on the closed order screen. Or if the user deletes a completed prepayment. Control is passed to the EmergencyCancelPayment() method when a payment that has already been made is canceled for an order that is not yet closed. For example, if the payment is fiscal and there are difficulties with printing the fiscal receipt and the payment is interrupted. If no specific logic is required for the second case, the ReturnPayment() method can simply be called from the EmergencyCancelPayment() method.

The methods accept the same parameters as the payment method. The transactionId is the same as that passed in the previously executed Pay() operation.

The methods are considered successfully completed if no exceptions of type PaymentActionFailedException or PaymentActionCancelledException occur during execution. If these exceptions occur, just like for payments, the return operation is interrupted.

Example code for integration with the hotel system. The return method cancels the transaction and prints a receipt with the canceled amount and saved data: whether the card was swiped or the number was entered.

[Serializable]
public class IsCardClass
{
    public bool IsCard;
}

public void ReturnPayment(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar)
{
    // Arbitrary methods are executed. For example, by transaction id, the payment is returned in some hotelSystem, which will return true if the payment was successfully rolled back and false if the return failed.
    var success = hotelSystem.ProcessReturnPayment(transactionId);
    if (!success)
        throw new PaymentActionFailedException("Failed to return payment.");

    // Get data saved in the payment item.
    var isCard = context.GetRollbackData<IsCardClass>();

    var slip = new ReceiptSlip
    {
        Doc =  new XElement(Tags.Doc,
            new XElement(Tags.Pair, "Return Amount", sum),
            new XElement(Tags.Pair, "Was the card swiped", isCard.IsCard ? "YES" :    "NO" ))
    };
    printer.Print(slip);
}

public void EmergencyCancelPayment(decimal sum, Guid? orderId, Guid paymentTypeId, Guid transactionId, [NotNull] IPointOfSale pointOfSale, [NotNull] IUser cashier, IReceiptPrinter printer, IViewManager viewManager, IPaymentDataContext context, IProgressBar progressBar)
{
    ReturnPayment(sum, orderId, paymentTypeId, transactionId, pointOfSale, cashier, printer, viewManager, context, progressBar);
}

The method ReturnPaymentWithoutOrder() is called when a return of goods occurs with an external payment type. The ability to return payment for goods without previously paid orders using external types has been available since Syrve version 6.2.2. To enable the option to select an external payment type in the UI for returns, the payment system must be registered with the optional parameter canProcessPaymentReturnWithoutOrder = true. That is,

var disposable = PluginContext.Operations.RegisterPaymentSystem(paymentSystem, true);

Unlike all the methods mentioned above, the ReturnPaymentWithoutOrder() method does not have the context of an order and previously completed payment. It is assumed that the amounts and payment types are sufficient to perform the return. During this operation, it is possible to show dialog boxes to the user and print receipts, just like for all the methods mentioned above.

Data Collection Methods

void CollectData(Guid orderId, Guid paymentTypeId, [NotNull] IUser cashier, IReceiptPrinter printer, UI.IViewManager viewManager, IPaymentDataContext context, UI.IProgressBar progressBar);
void OnPaymentAdded([NotNull] IOrder order, [NotNull] IPaymentItem paymentItem, [NotNull] IUser cashier, [NotNull] IOperationService operationService, IReceiptPrinter printer, UI.IViewManager viewManager, IPaymentDataContext context, UI.IProgressBar progressBar);
bool OnPreliminaryPaymentEditing([NotNull] IOrder order, [NotNull] IPaymentItem paymentItem, [NotNull] IUser cashier, [NotNull] IOperationService operationService, IReceiptPrinter printer, UI.IViewManager viewManager, IPaymentDataContext context, UI.IProgressBar progressBar);

If it is necessary to collect any data not at the moment of pressing the “Pay” button on the cash register screen, but at the moment of adding an external payment type item to the order, this can be implemented in the CollectData() method.

The OnPaymentAdded() method is called after adding a payment item to the order. The peculiarity of this method is that one of its arguments is IOperationService operationService. Unlike PluginContext.Operations, this instance has the authority to make changes to the current order. This is needed, for example, to set the amount for the added payment item or to add any dish to the order.

The method OnPreliminaryPaymentEditing() is called when editing preliminary payments. For this method, it is also possible to make changes to the current order through the IOperationService operationService argument. The method returns bool, the meaning of the returned value is as follows: is it possible to change the amount of the preliminary payment item from the UI after this method has completed.

Opening and Closing Cash Shift in SyrveFront

Some external payment systems need to perform certain actions on their side when opening and closing a cash shift in SyrveFront. For example, for banking systems, a reconciliation is required when closing the shift. To do this, you need to subscribe to INotificationService.SubscribeOnCafeSessionOpening and INotificationService.SubscribeOnCafeSessionClosing.

When opening and closing a cash shift, a new event is sent to the corresponding observer. Here is an example of code that prints the payment system key and whether the shift is opened or closed on the printer:

ctor
{
    // ...
    PluginContext.Notifications.SubscribeOnCafeSessionClosing(CafeSessionClosing);
    PluginContext.Notifications.SubscribeOnCafeSessionOpening(CafeSessionOpening)
}

private void CafeSessionOpening([NotNull] IReceiptPrinter printer, [NotNull] IProgressBar progressBar)
{
    PluginContext.Log.Info("Cafe session opening.");
    var message =
        "I cannot connect to my server and open the shift.";
    PluginContext.Operations.AddNotificationMessage(message, "SamplePaymentPlugin");
}

private void CafeSessionClosing([NotNull] IReceiptPrinter printer, [NotNull] IProgressBar progressBar)
{
    PluginContext.Log.Info("Cafe session closing.");
    var slip = new ReceiptSlip
    {
        Doc = new XElement(Tags.Doc,
            new XElement(Tags.Center, PaymentSystemKey),
            new XElement(Tags.Center, "Cafe session closed."))
    };
    printer.Print(slip);
}

If it is necessary to show the user any warning, this can be done using notifications. Exceptions that occur during the execution of CafeSessionOpening() and CafeSessionClosing() do not interrupt the operations of opening and closing the cash shift in SyrveFront. Moreover, if an exception occurs in the handler, it is considered broken and will not be called until the plugin is restarted.