Интеграция 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-механизмы.
  • Решение внедрено, работает и радует пользователей приложения и их клиентов.

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

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

ссылка на оригинал статьи https://habrahabr.ru/post/325950/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *