iikoFront API SDK

Внешние типы оплаты

Интеграция с внешними типами оплаты

Общая идея

Если реализовать интерфейс IExternalPaymentProcessor и зарегистрировать его соответствующим образом, в iikoRMS появится новая платёжная система. Упрощенно можно называть это внешним типом оплаты. Для плагинов, реализующих внешние типы оплаты, вводится специальное лицензирование.

Регистрация внешней платёжной системы

Плагин регистрирует платёжную систему с помощью IOperationService.RegisterPaymentSystem(...). В качестве обязательного параметра в этот метод передается paymentSystem – экземпляр класса, реализующего IExternalPaymentProcessor. В результате регистрации в iikoOffice появится новая платёжная система. Её можно увидеть в типах оплаты в разделе «Внешний тип оплаты» с наименованием IExternalPaymentProcessor.PaymentSystemName.

backPT

Если создать тип оплаты этой платёжной системы, на iikoFront на экранах кассы и предоплаты появится возможность выбрать этот внешний тип оплаты.

frontPT

Пояснения к терминам:

Интерфейс IExternalPaymentProcessor

Чтобы реализовать необходимую бизнес-логику по проведению и возврату платежа внешним типом оплаты, нужно реализовать интерфейс 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);
}

Здесь:

Метод проведения оплаты

Когда пользователь iikoFront выберет на экране кассы тип оплаты, задаст сумму и нажмёт кнопку «Оплатить», или когда пользователь внесёт предоплату определенным типом оплаты, управление придёт в метод 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);

Здесь:

Подробнее сигнатуру объектов можно найти в документации.

Например, если требуется реализовать интеграцию с гостиничной системой:

[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)
{
    // Показать в iikoFront окно ввода номера и прокатки карты
    var input = viewManager.ShowInputDialog("Введите номер или прокатайте карту", InputDialogTypes.Card | InputDialogTypes.Number);
    string room = null;
    string cardTrack = null;
    
    // Если был введен номер, то результат типа NumberInputDialogResult
    var roomNum = input as NumberInputDialogResult;
    if (roomNum != null)
        room = roomNum.Number.ToString();
    
    // Если была прокатана карта, то результат типа CardInputDialogResult
    var card = input as CardInputDialogResult;
    if (card != null)
        cardTrack = card.FullCardTrack;
    
    if (room == null && cardTrack == null)
        // Ничего не было введено, прекращаем операцию.
        throw new PaymentActionFailedException("Не было введено данных.");
    
    // Получаем заказ средствами API по id через IOperationService.
    var order = PluginContext.Operations.TryGetOrderById(orderId.Value);
    
    // Выполняем произвольные методы. Например, проводим платёж в некой hotelSystem, которая вернет имя гостя, если платёж «принят» и null, если платёж отклонён.
    var guestName = hotelSystem.ProcessPaymentOnGuest(cardTrack, room, order?.Number, transactionId, sum);
    if (guestName == null)
        // Платёж не прошёл, прекращаем операцию.
        throw new PaymentActionFailedException("Платеж не прошёл.");
    
    // Формирование квитанции для печати. Квитанция состоит из XElement
    var slip = new ReceiptSlip
    {
        Doc =  new XElement(Tags.Doc,
            new XElement(Tags.Pair, "Гость", guestName),
            new XElement(Tags.Pair, "Сумма", sum))
    };
    
    // Печать.
    printer.Print(slip);
    var cardInfoData = new IsCardClass { IsCard = card != null };
    var cardType = cardInfoData.IsCard
        ? "My Hotel System Card"
        : "My Hotel System Room";
    // Сохранение данных, которые будут показаны в отчётах.
    context.SetInfoForReports(room ?? cardTrack, cardType);
    // Сохранение данных, которые будут использованы для возврата оплаты.
    context.SetRollbackData(cardInfoData);
}

Исключение типа PaymentActionFailedException служит для прерывания операции оплаты. Пользователю iikoFront будет показан message этого исключения. Это имеет смысл, если возникли какие-либо проблемы при общении с внешним сервисом, оплата не может быть проведена и нужно проинформировать пользователя о причинах.

Для «тихого» прерывания операции можно воспользоваться исключением типа PaymentActionCancelledException. Это имеет смысл, если в процессе оплаты было показано диалоговое окно и пользователь нажал кнопку «Отмена».

Аргументы IReceiptPrinter, IViewManager и IPaymentDataContext «живут» только в процессе выполнения метода, после завершения метода экземпляры уничтожаются. Так что сохранять их в переменные не имеет смысла, т.к. вне метода их нельзя будет использовать.

Тихое проведение оплаты (Silent-оплата)

Иногда бизнесу нужны решения по оплате плагинными типами оплаты из самих плагинов, без входа на экран кассы iikoFront. Для этого плагин должен реализовать метод CanPaySilently процессора оплаты плагина. Результатом метода является ответ на вопрос «Имеет ли плагин возможность проводить оплату тихо?». Для того чтобы появилась такая возможность, необходимо чтобы в заказ предварительно был добавлен плагинный элемент оплаты. Для тихого проведения оплаты можно вызвать метод ProcessPrepay c флагом isProcessed равным false. В SDK приводится пример с использованием пользовательского класса со свойством 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);
}

В свою очередь iikoFront передает указанный для оплаты сериализованный класс в контекст оплаты(IPaymentContext), далее в методе CanPaySilently класс извлекается и десериализуется:

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

В зависимости от ответа возвращаемого значения метода CanPaySilently iikoFront вызовет Pay или PaySilently метод процессора плагина. Таким образом плагин сам решает как должен проводиться вновь добавляемый платеж.

Методы возврата оплаты

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);

Методы EmergencyCancelPayment() и ReturnPayment() вызываются, когда пользователь на iikoFront инициирует возврат проведённого ранее платежа.

В метод ReturnPayment() управление передаётся, когда на экране закрытого заказа нажимают кнопку «Частичный возврат чека» или «Удаление заказа». Или если пользователь удаляет проведённую предоплату. В метод EmergencyCancelPayment() управление передаётся, когда для ещё не закрытого заказа отменяют уже проведённую оплату. Например, если оплата фискальная и возникли трудности с печатью фискального чека и оплату прерывают. Если для второго случая не требуется специфической логики, то можно просто вызвать из метода EmergencyCancelPayment() метод ReturnPayment().

Методы принимают те же параметры, что и метод оплаты. transactionId тот же, что передавался в исполненную ранее операцию Pay().

Методы считаются успешно завершёнными, если в процессе выполнения не возникло исключений типа PaymentActionFailedException или PaymentActionCancelledException. Если эти исключения возникли, так же, как и для оплаты, операция возврата прерывается.

Пример кода интеграции с гостиничной системой. Метод возврата отменяет транзакцию и печатает чек с отменяемой суммой и сохранёнными данными: была ли прокатана карта, или был введён номер.

[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)
{
    // Выполняются произвольные методы. Например, по id транзакции платёж возвращается в некой hotelSystem, которая вернет true, если платёж успешно откатился и false, если возврат не удался.
    var success = hotelSystem.ProcessReturnPayment(transactionId);
    if (!success)
        throw new PaymentActionFailedException("Не получилось вернуть плату.");
    
    // Получаем данные, сохранённые в элементе оплаты.
    var isCard = context.GetRollbackData<IsCardClass>();
    
    var slip = new ReceiptSlip
    {
        Doc =  new XElement(Tags.Doc, 
            new XElement(Tags.Pair, "Возврат суммы", sum),
            new XElement(Tags.Pair, "Была ли карта", isCard.IsCard ? "ДА" :    "НЕТ" ))
    };
    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);
}

Метод ReturnPaymentWithoutOrder() вызывается, когда происходит возврат товаров внешним типом оплаты. Возможность возвращать оплату за товары без оплаченных ранее заказов внешними типами появилась начиная с версии iiko 6.2.2. Чтобы на UI возврата товаров появилась возможность выбрать внешний тип оплаты, нужно регистрировать платёжную систему с опциональным параметром canProcessPaymentReturnWithoutOrder = true. Т.е.

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

В отличие от всех упомянутых выше методов, метод ReturnPaymentWithoutOrder() не имеет контекста заказа и проведённой ранее оплаты. Предполагается, что суммы и типа оплаты достаточно для выполнения возврата. В процессе данной операции доступна возможность показывать пользователю диалоговые окна и печатать квитанции, так же, как и для всех упомянутых выше методов.

Методы сбора данных

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);

Если требуется собрать какие-либо данные не в момент нажатия на кнопку «Оплатить» на экране кассы, а в момент добавления элемента внешнего типа оплаты в заказ, то можно реализовать это в методе CollectData().

Метод OnPaymentAdded() вызывается после добавления элемента оплаты в заказ. Особенность этого метода в том, что одним из его аргументов является IOperationService operationService. В отличие от PluginContext.Operations, у данного экземпляра есть полномочия вносить изменения в текущий заказ. Это нужно, например, чтобы задать сумму для добавляемого элемента оплаты или вообще добавить какое-либо блюдо в заказ.

Метод OnPreliminaryPaymentEditing() вызывается при редактировании предварительных платежей. Для данного метода также доступна возможность вносить изменения в текущий заказ через аргумент IOperationService operationService. Метод возвращает bool, смысл возвращаемого значения следующий: доступно ли изменение суммы элемента предварительной оплаты с UI после завершения данного метода.

Открытие и закрытие кассовой смены в iikoFront

Некоторым внешним платёжным системам нужно выполнять на своей стороне определённые действия при открытии и закрытии кассовой смены на iikoFront. Например, для банковских систем при закрытии смены нужно проводить сверку. Для этого нужно подписаться на INotificationService.SubscribeOnCafeSessionOpening и INotificationService.SubscribeOnCafeSessionClosing.

При открытии и закрытии кассовой смены в соответствующий observer приходит новое событие. Пример кода, который при открытии и закрытии смены печатает на принтере ключ платежной системы и открыта или закрыта смена:

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 =
        "Я не могу подключиться к своему серверу и открыть смену.";
    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);
}

Если необходимо показать пользователю какое-либо предупреждение, можно сделать это с помощью уведомлений. Исключения, возникшие в процессе выполнения CafeSessionOpening() и CafeSessionClosing(), не прерывают операций открытия и закрытия кассовой смены на iikoFront. Более того, при возникновении исключения в обработчике, он считается сломанным и не вызывается до перезапуска плагина.