Shopserver. Схема обміну даними

Отримання документів обліковою системою від касового сервера

Інтеграція "Касовий сервер <-> Каса"

Shopserver. Схема обміну даними.

З моменту введення в експлуатацію касового сервера було проведено багато інтеграцій, тому прийшов час систематизувати накопичений досвід. А досвід нам каже, що просто мати грамотно організований та задокументований API недостатньо. Все ж таки інтеграцію проводять не комп'ютери, а люди. А людям потрібно розуміти принципи та мету інтеграції. А також, чому були прийняті ті, чи інші технічні рішення.
Інженерне мистецтво полягає в пошуку вдалого компромісу. І в нашому випадку це компроміс між потребою реалізувати складну систему, але взаємодіяти з нею простими засобами. Як відомо, HTTP пропонує нам наступні методи для взаємодії GET, POST, PUT, PATCH, DELETE. І розробники REST API залюбки їх використовують. Ідеологія REST полягає в тому, що методами (командами, що треба зробити) виступають як раз ці Get, Post, Put і т.д. А все інше в URL-рядку виступає в якості параметрів. Тобто в середині URL назва методу явно не фігурує. використовуються певні домовленості. Приклад класичної реалізації REST можна подивитись тут. Чи варто безумовно притримуватись стилю REST? На думку автора, тобто на мою думку - ні. 
Багаторічний досвід говорить про те, що достатньо всього двох HTTP методів - GET та POST. Мало того, при розробці API варто розглядати метод GET як окремий вироджений випадок методу POST. Використовуємо GET коли, по перше, ми хочемо отримати якісь дані не змінюючи їх на боці сервера, а по друге, оскільки параметри виклику методу передаються прямо в URL рядку і не шифруються, такі параметри не є секретними і перелік їх невеликий. Розглянемо той самий пейджинг. Коли передаємо початковий рядок та кількість рядків колекції об'єктів, які хочемо отримати від сервера. Наприклад, метод /api/v1/Cashiers/GetListPartial?startPosition=0&count=100, що повертає перелік касирів починаючи з першого (zero-based) та кількістю 100 рядків (якщо стільки є).
В інших випадках використовуємо POST. Тобто маємо такий собі REST навиворіт. Де важлива зрозуміла і нормальна назва методу класу, який відпрацює на сервері, а вже використання GET чи POST - носить допоміжний характер.
Що не так з PUT? А він порушує принцип Оккама. Він для нас зайвий. Рівно те саме (зберегти дані на сервері) можна зробити за допомогою метода POST, а вже сам сервер визначить, чи додавати нові дані (якщо їх немає в базі), або оновити, якщо такі дані в базі є. Наприклад, маємо довідник товарів. Розділення методів на окремі PUT і POST призводить до потреби заздалегідь знати, чи вже існує запис товару в базі даних. Тобто нам або на стороні клієнта потрібно вести паралельну базу всіх даних, або попередньо запитувати в сервера, чи існує запис, для конкретного товару). Маємо купу зайвих операцій. Це дорого з точки зору використання ресурсів. Відмова від PUT та PATCH одразу значно спрощує API та скорочує кількість методів.
Як ще спростити наш API? Як зменшити кількість потрібних методів? А ще як зменшити кількість помилок розробників-інтеграторів, які будуть використовувати наш API? А потрібно врахувати специфіку задачі. Отже починаємо говорити конкретно про API касового сервера Shopserver.

Фактично маємо дві інтеграції, які працюють в обидва боки - інтеграцію облікової системи з касовим сервером, та інтеграцію робочого місця касира також з касовим сервером. Розглянемо послідовно обидві інтеграції.

Інтеграція "Облікова система <-> Касовий сервер"

Маємо задачу - вивантажити дані для кас на касовий сервер, та отримати від кас чеки, замовлення, повернення, z-звіти та документи інших типів.


Вивантаження даних з облікової системи до касового сервера

Для кас потрібен набір довідників, а також залишки та ціни по товарах на торгових точках. Маємо задачу максимально спростити таке вивантаження. По перше, зменшуємо до необхідого мінімуму кількість веб-методів, по друге, зменшуємо до мінімально потрібного обсяг даних для вивантаження. Розглянемо, яким чином це досягається.
Зазвичай, класично, для вивантаження кожного довідника створюється окремий веб-метод. Наприклад, вивантаження довідника одиниць виміру, чи довідника груп товарів, чи покупців, чи касирів. Тобто потрібно створити багато різних методів, які передають різні дані. Одразу виникає просто ідея - вивантажувати дані у вигляді єдиного пакета, який ми назвемо контейнером реплікації. Отже, один метод - один пакет. В API для цього використовуємо метод /api/v1/AccountingSystem/SendReplication Структура самого контейнера реплікації детально описана в наданому посиланні. 
Що стосується зменшення обсягів даних - використовуємо стару перевірену реплікацію. Тобто передаємо не весь обсяг даних, а лише ті дані, що змінились, або додались з моменту попередньої реплікації. Зазвичай, на боці облікової системи це робиться наступним чином - для таблиці кожного довідника створюємо поле, де фіксуємо дату-час останнього змінення даних в конкретному рядку. Назвемо його, наприклад, LastModified. Після цього робимо запит, за яким відбираємо лише ті записи, що змінились з моменту попередньої реплікації. Включаємо такі записи до пакету реплікації і відправляємо на касовий сервер. Як показує досвід, довідники змінюються відносно рідко, на відміну від товарних залишків (колекція InventoryRecords в контейнері реплікації).

Видалення даних

Часто питають, як видалити ті чи інші записи на касовому сервері. Відповідь проста і категорична - ніяк. Бо це реплікація. Дані, які ви вивантажили на касовий сервер, вже були завантажені касами, і каси їх вже використовують. Тому видалення цих даних на касовому сервері втрачає сенс, вони залишаться на касах. Коли може виникнути потреба видалення окремих даних?
Я перебрав варіанти з минулого досвіду, і щось нічого актуального пригадати не можу. 
Розглянемо типові ситуації. Наприклад, некоректно назвали товар в довіднику облікової системи. Або віднесли товар до іншої групи. В цьому випадку просте виправлення даних в картці товару змінить дату-час в полі LastModified, товар буде включений до наступного контейнера реплікації і нормально оновиться на касовому сервері.
Зробили товару на іншу торгову точку? Оператори можуть, вони такі. Відкотили прихід в обліковій системі, але він вже потрапив на касовий сервер до таблиці InventoryRecords для "некоректної" каси, і, відповідно, на саму касу, куди потрапляти не потрібно. Як потім виловлювати ці "чужі" залишки і видаляти їх на касі?
Варіант може бути лише один - "початкова реплікація". Тобто, щоб бути впевненим, що на касовий сервер після всіх "ремонтів" облікової системи вивантажаться коректні дані, і вони розійдуться по касах, треба вивантажити повний комплект довідників з особливою ознакою в контейнері реплікації IsInitialReplication=true


В цьому випадку сервер попередньо видалить всі дані компанії і запише нові дані з контейнера реплікації. Це гарантує повну узгодженість даних між обліковою системою та касовим сервером, а потім - між касовим сервером та касами. Це своєрідний екстрений ремонтний режим. Використовувати потрібно лише у випадку нагальної потреби.

Рекомендований алгоритм реплікації даних з облікової системи до касового сервера

Ми вважаємо, що користувач вже має зареєстрований обліковий запис компанії в сервісах Base2Base. Якщо цього не зроблено, потрібно зареєструвати компанію за посиланням.

1. В обліковій системі нам треба мати змінну із датою останньої успішної реплікації. Змінну потрібно зберігати не в пам'яті, а в файлі, або в базі даних, щоб у разі перезавантаження системи ми не втратили значення дати-часу останньої реплікації. 
2. Для кожного довідника в таблиці створюємо поле LastModified і пишемо туди дату-час останнього оновлення або додавання даних в цей рядок таблиці.
3. Формуємо контейнер реплікації. Для цього читаємо дані довідників, що змінились з часу останньої успішної реплікації (поле LastModified) і пишемо їх в контейнер.
4. Авторизуємось методом /api/v1/AccountingSystem/Login та отримаємо дані користувача, яким працює ваша облікова система на касовому сервері. Нам в подальшому буде потрібен Bearer-токен з цього об'єкта (поле BearerToken). Зверніть увагу, що пароль користувача ми не передаємо у відкритому вигляді, ми передаємо лише його MD5-відбиток. Алгоритм отримання MD5-відбитку є стандартним і широко відомим. Скоріш за все знайдете або зразок коду, або готову бібліотеку.
5. Відправляємо контейнер реплікації до касового сервера за методом /api/v1/AccountingSystem/SendReplication Використовується стандартна JWT Bearer-авторизація.
6. За кодом 200 отримаємо об'єкт-звіт про успішну реплікацію з кількістю отриманих і збережених даних касовим сервером. За кодом 404 отримаємо інший об'єкт, що вміщує дані про виключну ситуацію з поясненням проблеми. Журнал обміну даними з касовим сервером можемо побачити тут.


Примітка
Замість методу /api/v1/AccountingSystem/SendReplication можна використати аналогічний метод /api/v1/AccountingSystem/SendReplicationZip, за яким можна відправити контейнер репліакації, стиснутий GZIP-алгоритмом. Для цього берем json контейнера реплікації, стискаємо його за GZIP алгоритмом та формуємо з отриманого масиву байтів Base64 рядок. Цей рядок передаємо методом POST. Рядок обов'язково повинен бути обгорнутий подвійними лапками.

У разі успішної реплікації ми можемо побачити довідник товарів на касовому сервері в кабінеті користувача за цим посиланням. Товарні залишки по торгових точках за цим посиланнямПерелік касирів та інші довідники. 

Отримання документів обліковою системою від касового сервера

Каси відправляють до касового сервера чеки, повернення, замовлення покупців та інші документи у вигляді контейнерів документів (не плутати з контейнерами реплікації). Контейнер документів може вміщувати один документ, наприклад, замовлення покупця, або декілька пов'язаних між собою. Наприклад, чек, який вміщує видаткову накладну на покупця та прибутковий касовий ордер, як оплату за товар від покупця). Більш детально про контейнер документів можна почитати за посиланням.
Наше завдання - імпортувати документи, що надходять з кас, до облікової системи.

Рекомендований алгоритм імпорту контейнерів документів від касового сервера до облікової системи

Типовий алгоритм знову ж таки повинен задіяти періодичну реплікацію, але вже в протилежний бік - треба імпортувати нові документи з касового сервера до облікової системи. Розпишемо оптимальну поведінку.
1. В обліковій системі нам потрібно мати змінну, яка зберігає максимальний Id контейнера документів, який був отриманий під час останньої успішної реплікації. Початкове значення буде 0, тип даних long (він же bigint, він же Int64, тобто ціле, 8 байтів).
2. Імпортуємо певну кількість контейнерів документів, наприклад, 128 штук. Варто зауважити, що це ми запитаємо максимальну кількість для отримання, в сервера може стільки і не бути, і сервер поверне, наприклад, 5 контейнерів, але не більше 128. Для цього скористаємось методом API /api/v1/AccountingSystem/DownloadContainers


Нам треба передати ряд параметрів
formId - це Id останнього контейнера, який був отриманий в попередньому сеансі реплікації (описаний в пункті 1 алгоритму).
recordCount - маскимальна кількість контейнерів, які хочемо отримати
appVersion - назва та версія облікової системи, яка зробила запит. Обов'язково вказуйте, вона журналізується, і в розділі сеансів будете бачити, який контейнер коли і якою системою був імпортований.
Accept-Language - параметр, що передається в розділі Headers http-запиту, для визначення мови, в якій бажаєте отримувати повідомлення про виключну ситуацію. Можна ігнорувати.
3. Імпортовані контейнери потрібно обробити обліковою системою та сформувати первинні документи - накладні, оплати, повернення. Як документи будуть інтерпретуватись - залежить від контексту системи і знаходиться поза межами цієї статті. Але варто зауважити, що під час імпорту документів потрібно забезпечити ідемпотентність. Простими словами, треба запобігти повторному імпорту тих самих документів. Кожен документ має унікальне поле DocumentGuid, під час імпорту документа потрібно перевіряти, чи існує документ з таким Guid в базі даних облікової системи. Якщо існує - то документ був імпортований раніше, і його потрібно ігнорувати.
4. Коли ми обробили контейнер документів, бажано відправити до касового сервер звіт про таку обробку за допомогою метода /api/v1/AccountingSystem/SendApproveResult. Таким чином касовий сервер отримає звіт від облікової системи про результат проведення конкретного контейнера документів.
5. Серед пакету імпортованих контейнерів знаходимо контейнер з максимальним ContainerId та запам'ятаємо значення в змінній (див. пункт 1).
6. Через певний проміжок часу, наприклад, 5 хвилин, знову запускаємо цикл реплікації, починаючи з першого пункту.

Інтеграція "Касовий сервер <-> Каса"

Під касою ми розуміємо будь який додаток, або систему, яка генерує чеки, замовлення, повернення від покупця, та відправляє ці документи до касового сервера. 


Реєстрація каси на сервері

Кожна каса має свій обліковий запис на касовому сервері. Так ми можемо централізовано керувати касами, бачити їх стан (працює, чи відключена), отримувати статистику, дивитись чеки та інші документи, сгенеровані цією касою.
За цим посиланням створимо новий обліковий запис для каси. Під касою ми розуміюмо будь який фронт-енд - додаток касира, інтернет-магазин, чи додаток на мобільному пристрої.
На сторінці з переліком кас тиснемо кнопку "Створити" і відкриється картка нової каси

Пропишемо назву "Каса 1" і натиснемо "Згенерувати код підключення".
Буде згенерований код підключення, який використаємо в подальшому.

Після отримання кода авторизації каса повинна викликати метод /api/v1/Bridge/SignUp і передати сгенерований на сервері п'ятизначний код.
Розглянемо об'єкт передачі даних для цього метода

ShopdeskGuid

Кожне робоче місце, тобто кожна каса або веб-сайт повинні одноразово сгенерувати свій унікальний Guid і додати його до об'єкта авторизації.


TrustedDeviceId

Зазвичай вміщує набір мак-адрес пристрою, але для веб-сайту варто просто повтрорити значення з ShopdeskGuid


OsVersion

Версія операційної системи пристрою. Бажано передавати.

AppName

Назва додатку, який реєструється на пристрої, або назва веб-сайту

AppVersion

Версія додатку

SignUpCode

Той самий код, який отримали на сайті під час створення облікового запису

ShopdeskVersion

Назва додатку та його версія (повторюються AppName та AppVersion). Не питайте чому, так склалось історично.

Після відправки json-об'єкта реєстрації касового місця за методом /api/v1/Bridge/SignUp на сервері відбудеться наступне. За згенерованим кодом авторизації буде знайдено обліковий запис для цього касового робочого місця. В цей запис пропишуться параметри ShopdeskGuid та TrustedDeviceId. У якості відповіді сервер відправить об'єкт успішної реєстрації за кодом 200, або об'єкт-повідомлення про виключну ситуацію з кодом 404. Наприклад, якщо вказали неіснуючий SignUpCode.
У ваипадку успішної реєстрації сервер поверне об'єкт
Де самим важливим параметром буде CompanyGuid. Це Guid облікового запису вашої компанії на серверах Base2Base. Він буде потрібен в подальшому для авторизації за методом Login.

Авторизація каси на сервері та отримання Bearer-токену

Для виклику робочих методів обміну потрібно отримати Bearer-токен. Це робиться за допомогою метода /api/v1/Bridge/Login
Об'єкт передачі даних на сервер виглядає наступнима чином
Для нас важливо передати ShopdeskGuid (який ми самі раніше згенерували і більше на цьому робочому місці не змінюємо), CompanyGuid (який отримали через метод SignUp), та TrustedDeviceId, про який також писали раніше.

У випадку успішної авторизації отримаємо об'єкт

Цей об'єкт фактично повторює відповідь метода реєстрації SignUp. Але SignUp ми викликаємо одноразово, а метод Login потрібно викликати кожен раз перед сеансом обміну даними.
Самим важливим для нас є поле Token, яке вміщує ключ для JWT Bearer-авторизації. В наступних методах покажемо, як він використовується.

Завантаження даних з касового сервера на касу

Кожна каса повинна отримати потрібний набір довідників для своєї роботи. Це довідник товарів, довідник покупців, довідник касирів, та інше. На відміну від контейнера реплікації, який вивантажує до касового сервера облікова система, каса повинна забрати лише той набір даних, який відноситься до конкретної торгової точки, яку обслуговує каса. Якщо довідник товарів є загальним для всієї системи, то набір товарних залишків із цінами каса отримує лише для своєї торгової точки.
Періодично каса повинна опитувати касовий сервер та отримувати оновлені дані. Для цього використовується метод /api/v1/Bridge/GetDictionaries. В параметрах через кому потрібно вказати Id підрозділів, які обслуговує каса, а також вказати дату-час, починаючи з якого ми хочемо отримати змінені дані. Якщо потрібно отримати дані з самого початку, просто вкажіть значення 1900-01-01. Варто зауважити, що каса може обслуговувати декілька підрозділів, які мають різне юридичне оформлення. Наприклад, загальна продуктова група може бути оформлена на один ФОП, а акцизні товари на інший. Тому Id цих підрозділів потрібно передати через кому. У випадку одного підрозділа, просто вказуємо його Id без коми.

Вивантаження чеків, повернень та замовлень з каси на касовий сервер

Після проведення документа на касі (або в інтернет-магазині, який знову ж таки з точки зору касового сервера є касою), каса повинна якмога швидше передати цей документ на касовий сервер. Щоб цей документ одразу був оброблений тими системами, яким це цікаво. Наприклад, обліковою системою, або службою доставки і т.д.
Для цього використовується веб-метод /api/v1/Bridge/SendFiles.

Через цей метод потрібно відправити колекцію документів, які накопичились з моменту минулого сеансу. Формат контейнера документів можна подивитись тут. На касових робочих місцях є певна специфіка. Інтернет може бути не завжди стабільним. Тому проведені касиром документи зберігаються у вигляді zip-файлів, а окремий незалежний процес періодично опитує теку зберігання і відправляє на касовий сервер накопичені файли у вигляді колекції. Назви та сенс полів описані та зрозумілі, тому не будемо повторюватись. Варто зупинитись на полі Content. Кожен контейнер документів є нормальним звичайним zip-архівом. Тому в поле Content нашого об'єкта зберігаємо масив байтів прочитаного файлу. Далі, серіалізуємо колекцію об'єктів (навіть якщо в колекції один об'єкт), наприклад, за допомогою NewtonSoft Json, і відправляємо на касовий сервер.
Документи, що були відправлені на касовий сервер, можна подивитись за посиланням.

Andriy Kravchenko

Admin, Writer, File Uploader

22.12.2024 23:56:33

Зареєструйтесь, щоб відправляти коментарі
An unhandled error has occurred. Reload 🗙