Интеграция Android-приложения с фискальным принтером и кардридером

    Всем привет! Сегодня мы хотим поделиться нашим опытом работы с периферийными устройствами на платформе Android.


    Представим себе...


    Вы пришли в горнолыжный прокат за лыжами и вам нужно рассчитаться картой и получить договор-оферту об оказании услуги и чек. Казалось бы, что может быть проще?
    image



    1. Делаем раз. Выбрали инвентарь (лыжи, палки, ботинки, шлем и маску).
    2. Сотрудник ловко достает смартфон и сканирует им баркоды на инвентаре.
    3. Тут же достает считыватель пластиковых карт, снимает денежные средства и замораживает залог.
    4. А вот чек и договор-оферта, которые напечатал принтер, прикрепленный на пояс.

    image


    Эх, мечты-мечты…


    В реальности все куда дольше, и вы сами прекрасно знаете почему.


    В одном месте вы выбираете инвентарь, потом идете в кассу, куда стекаются все со всех стоек проката, там оплачиваете, возвращаетесь к прокатчику, получаете у него инвентарь и, наконец-то готовы ко встрече с горой!


    image
    А в часы пиковых нагрузок эта история неминуемо превращается в сущий ад.


    1. Отстояли длиннющую очередь к прокатчику, выбрали инвентарь.
    2. Теперь постоим в общей очереди на кассу.
    3. Потом вернемся к прокатчику за законно оплаченным. И снова отстоим очередь. Вам ведь только спросить (забрать)? Да кого это волнует.

    Вот так скорость обслуживания из полутора минут растянулась на все 20, а в это время утренний вельвет снежного склона уже кто-то режет лыжами!


    Кто виноват, и что делать?


    Почему бы прокату не обустроить каждую стойку кассой? Да потому, что такая проблема не постоянна. Пиковые нагрузки случаются регулярно, но по большому счету не так часто – утро выходных и праздников, и это только если погода катальная. Поэтому инвестировать в кассовое оборудование с точки зрения владельцев явно затраты не супер оправданные. Эту задачу решает появление дополнительного мобильного сотрудника который раз, два, и помог очереди рассосаться.


    В кейсе с горнолыжкой достаточно было бы купить коробочное решение для интеграции смартфона с кассовым принтером и картридером, коих на рынке хватает. Ими пользуются и курьеры, которые доставляют заказы, и девушки, которые продают билеты на аэроэкспресс.


    Мы бы тоже могли ввести сумму и получить оплату, но у нас не прокат инвентаря!


    В системе заказчика с которой работали мы, буква закона требовала выдачи клиенту не просто чека на энную сумму, но еще и квитанции с подробным описанием услуги и уникальным присвоенным ей номером. То есть мы должны провести оплату, зарегистрировать это на сервере, получить уникальный номер транзакции, и затем распечатать чек, в котором отображается этот номер, полное описание услуги и баркод с закодированной информацией.


    Задача поставлена, переходим к реализации.


    Реализация


    1. Выбираем принтер и картридер


    Для начала определились, что надо использовать комплекс со смартфоном, который позволяет пользователю настраивать параметры услуги и взаимодействовать с сервером. Вопрос о выборе смартфонов для системы решился сам собой. Заказчик выбрал «рабочих лошадок» — смартфоны на базе Android. Ими оказались Huawei Honor 5C. Удобные недорогие устройства от китайского производителя. Главное, что Bluetooth есть и работает. А вот дальше надо было решить задачи посложнее. Чтобы все операции с продажей услуг проводились законно, нам был нужен фискальный регистратор. Это принтер с памятью, в которую записывается история операций по проведенным продажам.


    Мобильные фискальные принтеры (а мы помним, что наше решение должно быть мобильным!) выпускаются рядом российских компаний «Атол», «Инкотекс», «Счетмаш», «Штрих-М». Мы изучили их предложения, но в нашем решении принтер должен был печатать на широкой ленте (3” вместо 2”), поскольку нам нужно было разместить на чеке подробные данные об оказанной услуге. Широкая лента нашлась только у одного включенного в государственный реестр мобильного фискального принтера ШТРИХ-MOBILE-ПТК.


    С клиентов нам необходимо брать не только наличные платежи, но и списывать деньги с карты. А значит нужны картридер и провайдер такого решения, чтобы проводить оплаты и учитывать все проводимые таким способом средства. И таких решений на нашем рынке достаточно. Различаются они, пожалуй, типами моделей устройств для считывания, процентом комиссии за проведение эквайринга и… интеграциями с фискальными принтерами! Тут-то нам и попалось решение от iBox Pro, использующее картридер модели chip-n-pin с клавиатурой, а также, по счастью, интегрированное с мобильным фискальным регистратором ШТРИХ-MOBILE-ПТК.


    Компания предоставила нам тестовые устройства, и мы принялись за работу...


    2. Снимаем деньги


    С платежной системой iBox Pro мобильное приложение можно интегрировать простым (через intent-вызов) и сложным способом (через SDK). Мы выбрали сложный путь, но вовсе не потому, что нам нравится преодолевать трудности. А по другой важной причине. Тут понадобится лирическое отступление.


    В случае intent-вызова алгоритм такой:
    image
    Наш алгоритм выглядит иначе. Мы не можем просто взять, вбить сумму за услугу и распечатать чек. Нам нужно отправить данные на сервер, получить стоимость услуги, затем получить оплату от клиента, и после этого обратиться к серверу и получить уникальный код и затем обязательно отразить этот код в чеке.


    Поэтому мы разработали такой алгоритм:
    image
    В итоге с помощью SDK iBox (свежую версию их эволюционирующего SDK можно скачать с GitHUB) мы встроили вызовы к iBox в наше мобильное приложение. Бонусом получили более удобный для пользователя единый интерфейс – на этапе оплаты человеку не приходится переключаться на сторонний интерфейс iBox, все процессы происходят только в рамках нашего мобильного приложения.


    Пример работы с iBox SDK
    // С помощью вызовов методов PaymentController-а происходит передача команд устройству:
    
    // Устанавливаем однофакторную авторизацию.
    PaymentController.getInstance().setSingleStepEMV(true); 
    
    // Задаем логин и пароль кассира через которого будет проводиться оплата
    PaymentController.getInstance().setCredentials(loginInfo.userName, loginInfo.userPassword);
    
    // Среди сопряженных с телефоном устройств находим то, 
    // которое было указано в качестве картридера. 
    // Индекс этого устройства передаем PaymentCotroller-у.
    ReaderBluetoothInfo readerBluetoothInfo = settingsService.getReaderBluetoothInfo();
    
    List<BluetoothDevice> devices =
        PaymentController.getInstance().getBluetoothDevices(getView().getContext());
    
    for (int i = 0; i < devices.size(); ++i) {
        if (devices.get(i).getAddress().equals(readerBluetoothInfo.readerAddres) &&
             devices.get(i).getName().equals(readerBluetoothInfo.readerName)) {
                  PaymentController.getInstance().setReaderType(
                        getView().getContext(),
                        PaymentController.ReaderType.WISEPAD, i, null);
              }
         }
    }
    
    ...
    PaymentDialog paymentDialog = new PaymentDialog();
    
    // Перед непосредственным проведением платежа:
    // Передаем в контроллер листенер, на который будут приходить статусы и результат платежей от ibox. В нашем случае это диалог, который отображает статус оплаты.
    PaymentController.getInstance().setPaymentControllerListener(paymentDialog); 
    PaymentController.getInstance().enable();
    
    // Запускаем платеж по карте
    PaymentController.getInstance().startPayment(getContext(), mPaymentContext);
    
    ...
    // Метод который вызывает PaymentController во время проведения оплаты. 
    public void onReaderEvent(PaymentController.ReaderEvent event) {
        switch (event) {
             case CONNECTED :
             case START_INIT :
                 lblState.setText(R.string.reader_state_init);
                 break;
             case DISCONNECTED :
                 stopProgress();
                 lblState.setText(R.string.reader_state_disconnected);
                 break;
             case SWIPE_CARD :
             case TRANSACTION_STARTED :
                 startProgress();
                 break;
    
              ...
              case EJECT_CARD :
                  stopProgress();
                  lblState.setText(R.string.reader_state_eject);
                  break;
              case BAD_SWIPE :
                  Toast.makeText(mActivity, R.string.reader_bad_swipe, Toast.LENGTH_LONG).show();
                  break;
              case LOW_BATTERY :
                  Toast.makeText(mActivity, R.string.reader_low_battery, Toast.LENGTH_LONG).show();
                  break;
              default :
                  break;
         }
    }

    3. Печатаем документы


    После фиксации факта продажи необходимо выдать клиенту чек, а иногда и дополнительные документы – договор-оферту, бланк заказа или гарантийное письмо. Вопрос “на чем будем печатать?” — если у пользователя на поясе висит мобильный принтер — пусть печатается на нём. Поэтому, мы заставили принтер печатать помимо фискальных чеков и другие документы.


    С принтером можно работать через официальную библиотеку от Штрих-М с документацией, которую они создали для удобства работы с JPOS, а также с помощью небольшой библиотечки-примера от энтузиастов.


    Осторожно, грабельный лес!


    При печати документов на мобильном принтере мы столкнулись с двумя случаями, когда матрица давала сбой:


    • В сочетании с некоторыми смартфонами, которые мы использовали для тестирования, скорость работы принтера падала катастрофически. Нас это очень тревожило, так как по стандарту обслуживания заказчик требовал от нас уложиться в 1,5 минуты со всем процессом от оформления услуги до выдачи чека. По счастью, целевой девайс печатал по Bluetooth без проблем, и эту загадку мы решили оставить неразгаданной.


    • На боевом тестировании абсолютно одинаковые фискальные принтеры вели себя по-разному: из-за внутренних настроек некоторые из них при печати баркода сдвигали баркод на документе вправо, отрезая часть информации. Решить удалось только применив волшебный workaround :) – уменьшили некоторые элементы чека и баркода без потери качества считывания сканером.

    В итоге получился вот такой код печати баркода, если кому интересно.
    ShtrihFiscalPrinter printer = new ShtrihFiscalPrinter(new FiscalPrinter());
    
    PrinterBarcode printerBarcode = new PrinterBarcode();
    printerBarcode.setText(boardingPass.barcodeText); //Информация, зашифрованная в баркоде.
    printerBarcode.setLabel("");
    // Устанавливаем ширину, что пропорционально меняет размер баркода.
    // Возможные значения параметра - см. документацию к принтеру.
    printerBarcode.setBarWidth(2); // 2 - small width
    printerBarcode.setType(PrinterBarcode.SM_BARCODE_PDF417);
    
    Map<EncodeHintType, Object> parameters = new HashMap<>();
    
    // В качестве альтернативы вместо изменения размера элементов баркода можно уменьшить его по ширине и увеличить по высоте.
    // Поменять измерения баркода можно в строчке ниже.
    parameters.put(EncodeHintType.PDF417_DIMENSIONS, new Dimensions(5, 5, 2, 60)); 
    
    printerBarcode.addParameter(parameters);
    
    printer.printBarcode(printerBarcode);
    

    • Ещё одна странная проблема всплыла при работе с iBox. При работе с нашими смартфонами устройство могло войти в спящий режим и не выходило из него. Проявлялось только с Huawei и вылечилось с помощью установки последней версии прошивки картридера.

    4. Печатаем фискальный чек


    По умолчанию фискальный принтер выдает чек от платежной системы с минимумом информации. Нас это не устроило. Во-первых, нам нужен был красиво оформленный чек, который выглядел бы аналогично чекам, утвержденным в других системах заказчика. Во-вторых, нам нужно было совместить чек с квитанцией об оказании услуги и подробной информацией о ней. Итого – с помощью SDK принтера мы сделали красивый документ чек-квитанцию. А потом долго и мучительно на тестировании один за другим печатали чеки, меняя параметры, пока, наконец, реальная бумажка не стала выглядеть на всех принтерах одинаково и так, как нужно.


    Вот код для печати фискального чека:
    private void printTicket(PrinterInfo printerInfo, boolean isRefund, String agentId) throws Exception {
        ShtrihFiscalPrinter printer = new ShtrihFiscalPrinter(new FiscalPrinter());
    
        //Здесь заполняется таблица налогов внутри принтера
        final String NO_TAX = "0";
        final String TEN_PERCENT_TAX = "1000"; 
        printer.setVatValue(1, NO_TAX);
        printer.setVatValue(2, TEN_PERCENT_TAX); //НДС 10%
        printer.setVatValue(3, NO_TAX);
        printer.setVatValue(4, NO_TAX);
        printer.setVatTable();
    
        printer.setHeaderLine(1, StringTools.appendStrings("", "*", LINE_LENGTH), false);
        printer.setHeaderLine(2, getHeader("ООО \"Хорошая компания\""), false);
        printer.setHeaderLine(3, getHeader("Тридевятое государство"), false);
        printer.setHeaderLine(4, getHeader("улица Пушкина, \nДом колотушкина"), false);
        printer.setHeaderLine(5, getHeader("+7(XXX)XXX-XX-XX"), false);
        printer.setHeaderLine(6, StringTools.appendStrings("", "*", LINE_LENGTH), false);
    
        if (isRefund) {
            printer.setFiscalReceiptType(jpos.FiscalPrinterConst.FPTR_RT_REFUND);
        } else {
            printer.setFiscalReceiptType(jpos.FiscalPrinterConst.FPTR_RT_SALES);
        }
    
        printer.beginFiscalReceipt(true);
    
        printLine();
    
        double priceSum = 0;
        //PrinterEmdData содержит информацию о купленной пассажиром услуге.
        for (PrinterEmdData printerEmdData : printerInfo.getPrinterEmdDatas()) {
            String description = getDescription(printerEmdData, printerInfo.isCashierFormat());
            int tax = 0;
            final int TEN_PERCENT_NDS = 10;
            final int SECOND_TAX_SLOT = 2;
            if (printerEmdData.taxValue == TEN_PERCENT_TAX) {
                 tax = SECOND_TAX_SLOT; // Здесь указываем, что нас интересует налог 10% (он у нас стоит во 2-ом слоте таблицы налогов)
            }
            priceSum += printerEmdData.price;
    
            if (isRefund) {
                printer.printRecItemRefund(description, 0, 0, tax, (long) printerEmdData.price, "");
            } else {
                printer.printRecItem(description, 0, 0, tax, (long) printerEmdData.price, "");
            }
    
        }
    
        printLine();
        if (printerInfo.isCard()) {
            printer.printRecTotal((long) priceSum, (long) priceSum, "1");
        } else {
            long cashIn = (long) priceSum;
            if (printerInfo.getCashIn() > 0) {
                cashIn = (long) printerInfo.getCashIn();
            }
            printer.printRecTotal((long) priceSum, cashIn, "");
        }
    
        printer.endFiscalReceipt(true);
    }

    5. Обеспечиваем отказоустойчивость системы


    Конечно, мало просто сделать интеграцию в системе, важно сделать эту систему надежной. Для нас важно было обеспечить механизмы отката назад с любого этапа. Например, платежный сервис подтвердил приложению снятие денег, приложение запросило у сервиса бронирования услуг уникальный номер услуги, и тут пропала связь с сервером. И вот на этом этапе нужно сделать Fallback-механизм, который позволит повторить запрос на сервер или вернуть деньги на карту.


    Вариантов подобных ошибок оказалось множество: при снятии денег рано вытащили карту, неправильно ввели пин-код, при попытке распечатать чек закончилась кассовая лента, принтер разрядился и так далее. Границы необходимой отказоустойчивости на каждом этапе мы определяли в ходе тест-сессий с участием опытных кассиров. В результате для каждой ошибки были разработаны необходимые Fallback-механизмы.


    Возможные ошибки в процессе приема денег и распечатки чека


    image


    Итого:


    • Сделали отказоустойчивую интеграцию смартфон + картридер + фискальный принтер.
    • Протестировали в боевых условиях и наладили еще раз все Fallback-механизмы.
    • Решение внедрено, работает и радует пользователей приложения и их клиентов.

    Мы на собственном примере убедились, что решить задачу приёма платежей с помощью мобильных устройств можно, даже если на стороне сервера необходимо вести строгий учёт каждой операции. Может быть, прочитав статью, кто-то увидит возможность избавить людей от потери времени в очередях и в какой то другой сфере.


    Даешь больше комфорта клиентам, которыми можем оказаться и мы с вами!

    True Engineering
    72,00
    Специалисты по цифровой трансформации бизнеса
    Поделиться публикацией

    Комментарии 15

      0

      А с этими кард-ридерами в теории можно работать как-то автономно, чтобы прочитать номер карты? Или сначала надо выбирать провайдера (платежный шлюз?), а уж потом с ним решать вопросы, что из считанной карты он может передать в приложение?

        +2
        С кард-ридера вы получите шифрованный пакет с карточными данными, расшифровать его может только тот кто выдал вам этот кард-ридер. Но даже если вы расшифровали то вам нужно провести транзакцию через процессинговый центр банка. Т.е. нужна интеграция с банком и с ридером, а еще (раз вы работает с карточными данными) то вы еще и под PCIDSS попали, со всеми вытекающими…

        Платежный шлюз, как правило, предоставляет SDK|приложение для ридера и для интеграции со шлюзом — т.е. приложению нужна интеграция (простая) с SDK от платежного шлюза.

        Тот же SDK/приложение может также содержать интеграцию с нужным фискальным принтером.

        Т.е. вы просто кидаете сумму и описание платежа в SDK|приложение, а в ответ получаете информацию о проведенном платеже (при этом принтер печатает фискальный чек) или об ошибке, а все fallback-и приличные интеграторы прописывают уже внутри своего sdk|приложение.
          0

          То есть просто купить на рынке кард-ридер и читать информацию с карты не получится, как я понял :( У нас сейчас проблема в том, что не можем придумать надежный и безопасный способ передачи информации о карте клиента платежному шлюзу с девайса оператора. С девайса клиента решили вопрос — во фрейме окно шлюза, куда клиент вводит номер карты и CVC, а шлюз передаёт нам токен для дальнейшей работы с картой. Но то же самое сделать в приложении для наших операторов смысла не видим по психологическим причинам — есть подозрение, что мало кто из клиентов захочет вводить номер и CVC в приложении на чужом девайсе, а вот к кард-ридерам народ привычный.

            0
            На рынке вам никто и не продаст кард-ридер, если только на черном…
            Ридер можно купить (официально у поставщиков), но есть одно «НО». Там ключи которые прописывает производитель и этими ключами можно расшифровать пакет с ридера. И там будет вся инфа с карты — только вот вам в таком случае надо свое приложение и вашу организацию проводить через PCI DSS аудит. А шлюз (обычно) может принять и карточные данные (в открытом виде).

            Но транзакция может идти в разных режимах — если проверка PIN идет в ридере (на самой карте), то там только криптограмму специальную надо передать вместе с данными карты (по сути это подтверждение проведения аутентификации плательщика устройством). Но если требуется проверка через банк эмитетнт, то тут уже идет PIN блок (шифрованный). Только вот PIN блок — его раскрывать — еще более строгий и дорогой уровень сертификации. А без PIN — только оплата по NFC на сумму не выше 1000р.

            В общем тут очень много нюансов. И если не охота с этим возится то лучше купите решение от шлюза. Сейчас все нормальные шлюзы предлагают решения для мобильной торговли (с кард-ридером и без необходимости серьезной сертификации клиентского приложения ибо через него все карточные данные идут в закриптованном виде, расшифровываются только на стороне шлюза). Если ваш шлюз не предлагает мобильного решения — переходите к другому.
        0
        В реальности все куда дольше, и вы сами прекрасно знаете почему.
        А действительно, почему — автоматизации нет? А если же просто хозяин проката не уважает клиентов? — тогда автоматизация тут не поможет — хозяин урежет (экономия!!) ещё какой-то ресурс, и всё вернётся на круги своя.
          0
          а когда был сдан этот проект?
          А то ведь 58ФЗ уже начал помаленьку работать, а там уже кассы без фискального накопителя. А с отправкой данных на сервер провайдера фискальных данных.
          т.е технически хорошее решение — экономически не понятно
            0
            54-ФЗ. Как раз теперь в ККТ или в фискальных регистраторах будут фискальные накопители. Раньше было ЭКЛЗ.
            Схему представленную в статье показывали на конференции Атол.
            Насколько я помню решение для курьерских служб и разъездной торговли.
              0
              В нашем случае соблюдение новых требований по фискальному учету ложится на плечи эквайрингового оператора — iBox предоставили нам и картридеры, и фискальники, и сейчас обновляют принтеры под соответствие обновленным правилам 54-ФЗ. Если бы не ребята, нам (в смысле заказчику) пришлось бы все обновлять самим, а тут «полный сервис».
              0
              какой-то странный у вас flow

              мой опыт
              неделю назад бы на покатушках
              в кассе обычный кассовый аппарат с терминалом для карточек
              1 заплатил за скипас и аренду амуниции
              2 получил чек со штрихкодом

              аренда, там стоят терминалы
              3 завел свой аккаунт по номеру прав (можно и по кредитке)
              4 отсканировал код с чека
              5 указал рост, размер обуви, вес
              6 распечатал бумагу с кодами на аренду
              7 по этой бумаге получил шлем и боты
              8 проверил боты по ноге
              9 получил лыжи по бумаге
                0
                какой-то странный у вас flow

                вполне распространенный вариант
                думаю что связано с тем, что в выходные/праздничные запросто может не быть нужного размера, а списать деньги а потом отправить клиента за возвратом как-то не очень
                +1
                просто дам Вам ссылку на официальный репо Штрих-М на github с актуальным java драйвером: https://github.com/shtrih-m/javapos_shtrih
                  0
                  Спасибо! Полезно! обновим в статье, с Вашего позволения. Ваша библиотека с документацией будет более понятна.
                  0
                  С понедельника начну интеграцию печати из android-приложения на вот такую беду… не спрашивайте почему именно такой, шеф пришел, сгрузил, дал цу разобраться и интегрировать печать -_-
                    0
                    Про почему спрашивать не будем. Всякое бывает :) Просто напомним про то, что если вы собираетесь эту штуку применять в России для печати чеков, вам нужен фискальник. Да не простой, а соответствующий обновленным требования 54-ФЗ, как верно заметили bi4ara и RuJet. А ваш судя по краткому описанию, не таков. За рубежом таких проблем нет. Сами делали зарубежный проект, и простой термопринтер закрывал все наши потребности.
                      0
                      Не РФ, живу и работаю в маленьком огрызке земли недогосударстве ПМР, что между Молдовой и Украиной. Что самое не позитивное в задании, так это полная неопределенность в целях использования, они ещё сами не знают зачем им нужна печать =\ Может так случиться, что будет «разберись — разобрался — интегрируй — интегрировал — полежало — ну… ладно, это нам не надо, выпиливай из проекта»)

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое