Shopserver і кабінет Base2Base: предметна, архітектурна та технічна модель системи
Shopserver і кабінет Base2Base: предметна, архітектурна та технічна модель системи
Цей матеріал поєднує предметну модель Base2Base, архітектурні рішення та конкретну технічну реалізацію Shopserver і кабінету користувача. Він призначений як для людей, що вивчають систему, так і як структуроване джерело знань для AI-асистента Base2Base.
Версія 3.0. Стан коду: Base2Base.Site.zip, 21.08.2026. Предметний контекст: експорт статей Support Center із бази даних Base2Base, 192 записи. Архітектурний шар побудовано повторним статичним аналізом solution.
Для чого створено цей документ
Цей текст призначений не стільки для людини-розробника, скільки для мовної моделі, яка повинна розуміти систему Base2Base так, як її розуміє досвідчений консультант або архітектор. Тому документ навмисно починається з предметного змісту, а технічні назви класів, сторінок, API та сутностей використовуються як другий шар — для прив'язки людських понять до фактичної реалізації.
Основне правило цього опису: для кожної важливої теми розглядаються три шари: предметний зміст, архітектурне рішення та конкретна реалізація. Тобто важливо не лише знати, що таке товарний залишок або контейнер, і не лише знайти відповідний клас, а й зрозуміти, чому система розділена саме так, де проходить межа відповідальності та які властивості це рішення дає всій екосистемі.
Джерела знань мають різну роль. Статті Support Center пояснюють терміни, бізнес-процеси і практичну модель роботи. Вихідний код визначає фактичні межі поточної реалізації, маршрути даних, класи, API та зв'язки між компонентами. Якщо стаття описує загальну облікову модель Trade Control Utility, це не означає, що Shopserver повторює всю цю модель у своїй базі. У таких випадках документ чітко розділяє первинний облік і оперативну репліку даних, яка потрібна касам, сайтам та іншим фронт-офісним системам.
Як читати твердження про причини архітектурних рішень
У вихідному коді далеко не кожне рішення супроводжується коментарем «ми зробили так тому, що...». Тому в цьому документі розділяються три рівні впевненості.
Підтверджений факт — безпосередньо видно з коду, конфігурації або статей. Наприклад, сайт має reference на AndriyCo.Shopserver.Proxy, але не має reference на AndriyCo.Shopserver.Data; DocumentContext і DictionaryContext використовують різні connection string; після приймання контейнерів Shopserver публікує ContainersUpdatedMessage через MassTransit.
Архітектурний висновок — причина не записана одним реченням у коді, але досить однозначно випливає зі структури рішення. Наприклад, винесення Proxy у netstandard2.0 та відсутність залежності Site від Data свідчать про свідоме відділення транспортного контракту від внутрішньої серверної моделі.
Припущення — можливе пояснення, для якого коду недостатньо. Такі твердження в цьому документі не подаються як факт. Якщо мотивацію неможливо надійно встановити, текст описує наслідок рішення, а не вигадує історію його появи.
Це важливо для майбутнього RAG: асистент повинен мати змогу пояснити архітектуру, але не повинен приписувати авторам мотиви, яких немає в джерелах.
---
1. Base2Base як набір взаємопов'язаних систем
Детальніше: Архітектура сервісів Base2Base; Автоматизація продуктового магазину.
Base2Base — це не одна програма, а контур сервісів і клієнтських застосунків, які вирішують різні частини задачі торгового підприємства.
У найпростішій схемі продуктового магазину є два базові рівні. Trade Control Utility (TCU) виконує роль back-office, тобто облікової системи: прихід товару, товарні запаси, документи, ціноутворення, взаєморозрахунки, переоблік, виробництво, замовлення постачальникам, звітність. Shopdesk виконує роль front-office: касир швидко знаходить товар, формує чек, приймає оплату, працює з покупцем і торговим обладнанням.
Між цими двома рівнями потрібен надійний обмін. До контуру можуть додаватись інші фронт-офісні системи: інтернет-магазини, QR Menu, Data Terminal, Cookdesk, ваги з друком етикеток, зовнішні сайти. Крім того, є Trade Control Center (TCC) — CRM та програма лояльності, яка працює з покупцями, бонусами, маркетинговими акціями та іншими CRM-сценаріями.
Саме тут з'являється Shopserver. Його предметна роль — не бути ще однією обліковою системою, а бути центральним оперативним хабом між обліковим контуром і фронт-офісом. Облікова система передає йому довідники, ціни, залишки та інші потрібні дані. Каси й сайти отримують ці дані від Shopserver. У протилежний бік каси та сайти передають чеки, замовлення, повернення й інші документи, а облікова система забирає ці документи для проведення у своєму первинному обліку.
Ця роль важлива для правильного розуміння практично всіх сутностей Shopserver. Наприклад, InventoryRecord у Shopserver — це не вся історія партійного обліку TCU. Це оперативний стан товару в конкретному підрозділі, необхідний касі, сайту, QR Menu та іншим клієнтам. Контейнер документів у Shopserver — не заміна документу TCU, а транспортна й журналізована форма, за допомогою якої бізнес-операція переходить між системами.
З погляду користувача кабінет Base2Base є веб-вікном у цей серверний контур. Через нього можна бачити товари й залишки, реєструвати каси та інші пристрої, переглядати контейнери документів і сеанси реплікації, налаштовувати сайти, QR Menu, Quick Menu, дивитись робочі зміни й оперативну аналітику.
---
Архітектурний вимір: Shopserver як антизв'язувальний шар між back-office і фронт-офісом
Головне архітектурне рішення полягає в тому, що Shopdesk, сайт, QR Menu та інші оперативні клієнти не повинні знати внутрішню модель TCU і не повинні мати прямий доступ до її бази. Зворотне також справедливе: TCU не повинна реалізовувати окремий протокол взаємодії з кожним типом фронт-офісу.
Shopserver перетворює топологію «кожен з кожним» на схему з центральним вузлом. Це зменшує кількість прямих залежностей. Якщо існує N back-office/службових джерел і M фронт-офісних клієнтів, без посередника потенційно виникає N×M інтеграційних зв'язків. Через Shopserver більшість компонентів знають лише один контракт — контракт із сервером.
Це рішення дає ще одну властивість: розходження життєвих циклів. TCU може оновлюватись, мати складну партійну модель й виконувати важкі облікові розрахунки, тоді як каса повинна швидко продавати товар навіть при нестабільному зв'язку. Shopserver дозволяє передавати між цими світами не всю внутрішню модель, а лише потрібну оперативну проєкцію: довідники, ціни, залишки, документи та стани обміну.
У коді ця роль проявляється не одним «центральним класом», а сукупністю механізмів: AccountingSystemController приймає реплікацію від облікової системи; BridgeController та інші front-office API працюють із касами; ContainersController і DocumentContext зберігають бізнес-події; DictionaryContext зберігає оперативні довідники; SignalR і MassTransit доставляють зміни іншим учасникам.
Отже Shopserver варто розуміти не як «веб-API над SQL Server», а як інтеграційний домен, що володіє оперативним станом і протоколом взаємодії між системами.
2. Компанія — верхня межа даних
Практично всі бізнес-дані Shopserver належать певній компанії. Компанія — це верхній tenant-рівень, який відокремлює дані одного клієнта Base2Base від іншого.
Для користувача це означає просту річ: коли він працює в кабінеті від імені конкретної компанії, він бачить товари, підрозділи, каси, залишки, контейнери документів та налаштування саме цієї компанії. Перемикання контексту компанії змінює весь робочий простір.
На технічному рівні AndriyCo.Site2023 не читає таблиці Shopserver напряму. Сайт працює через контракти AndriyCo.Shopserver.Proxy і HTTP API. Bearer-токен передається в Shopserver, де ShopserverControllerBase виконує авторизацію. DictionaryContext.Authorize визначає користувача і встановлює CurrentCompany. Після цього доменні запити в AndriyCo.Shopserver.Data фільтрують дані за CurrentCompany.CompanyId.
Тому ізоляція компаній є не просто UI-фільтром. Вона є частиною серверної моделі доступу до даних. Це важливо для відповідей на питання на кшталт «чому я не бачу касу іншої компанії», «звідки береться поточна компанія» або «чи можна випадково отримати чужі залишки через API».
У коді цей принцип видно в багатьох доменних класах: GoodsItem, InventoryRecord, Department, Cashier, WebSite, QrMenu, QuickMenu, Presentation та інші сутності зберігають або перевіряють CompanyId.
---
Архітектурний вимір: tenant-контекст визначається на сервері
Для multi-tenant системи небезпечно будувати безпеку на домовленості «клієнт передасть правильний CompanyId». У Shopserver обрано іншу модель: bearer identity перетворюється на серверний контекст користувача, а DictionaryContext.Authorize встановлює CurrentCompany.
З цього випливає важливий принцип: CompanyId у бізнес-запиті не є головним джерелом істини щодо tenant-а. Основне джерело — авторизований користувач і його поточна компанія. Саме тому в доменних методах часто видно порівняння із DictionaryContext.CurrentCompany.CompanyId.
Архітектурна користь подвійна. По-перше, UI не може випадково «забути» фільтр компанії й отримати всі рядки. По-друге, однаковий захист діє для різних клієнтів API: сайту, облікової системи, службових компонентів. Це переносить tenant isolation із presentation layer у сервісний шар.
DictionaryContext при цьому містить не тільки DbSet, а й User, CurrentCompany, конфігурацію та логер. Тобто фактично контекст виконує роль request-scoped доменного середовища, де доступ до даних пов'язаний із авторизаційним контекстом. Це дещо ширша роль, ніж у «чистого» EF DbContext, але саме вона дозволяє численним статичним/доменним методам перевіряти належність об'єктів компанії без передачі CompanyId через кожен рівень виклику.
Компроміс такого підходу — сильніше зчеплення доменної моделі з DictionaryContext. Перевага — центральне і важко обхідне правило tenant-ізоляції.
3. Підрозділ компанії: не лише магазин або склад
Детальніше: Довідник підрозділів; Картка підрозділу компанії.
Один із найважливіших термінів предметної області — підрозділ компанії. У статтях Support Center прямо пояснюється, що торгова точка, склад і банківський рахунок поєднані спільним поняттям «підрозділ». Підрозділом може бути магазин, склад, офіс, виробничий цех, банківський рахунок або інше місце, де відбуваються операції та/або зберігаються цінності.
Це універсальна абстракція. Вона дозволяє не будувати окрему модель для «магазинів», окрему для «складів» і ще одну для інших місць обліку. Реальна роль підрозділу визначається його функціями та налаштуваннями.
Для Shopserver найбільш важливими є підрозділи, пов'язані з товарними запасами, касами та каналами продажу. Наприклад:
- магазин може мати власні товарні залишки й декілька кас Shopdesk;
- окремий склад може зберігати запас, з якого поповнюються магазини;
- виробничий цех у TCU може бути окремим підрозділом, а готова продукція передаватись на інший склад;
- інтернет-магазин може продавати товар одразу з декількох підрозділів;
- підрозділ, на який надходить замовлення для обробки, не обов'язково збігається з підрозділом фактичного відвантаження.
В обліковій системі підрозділ має глибший зміст: для нього можна окремо закрити звітний період, вести товарний запас, проводити документи. Стаття про закриття періоду підкреслює, що один магазин уже може мати закритий період, тоді як інший магазин тієї самої компанії ще працює з тією самою календарною датою.
У Shopserver довідник підрозділів представлений сутністю Department у AndriyCo.Shopserver.Data і контрактами Proxy. Кабінет використовує DepartmentListDataSource, а API — DepartmentsController. Підрозділи також постійно зустрічаються в інших функціональних областях: InventoryRecord прив'язує товарний запас до DepartmentId; Shopdesk прив'язується до торгової точки; WebSite використовує набір підрозділів для розрахунку доступності, цін та залишків.
Для RAG важливо не зводити DepartmentId до «ідентифікатора складу». Правильне пояснення: це ідентифікатор універсального підрозділу компанії; в конкретному сценарії цей підрозділ може виконувати роль магазину, складу, офісу, виробничого цеху або іншого центру операцій.
---
Архітектурний вимір: універсальний Department замість окремих моделей «магазин», «склад», «цех»
Тут предметне рішення одночасно є архітектурним. Система моделює місце операції або зберігання через одну універсальну сутність Department, а конкретна роль визначається налаштуваннями та зв'язками.
Таке рішення зменшує кількість спеціальних випадків. InventoryRecord може однаково посилатися на склад, магазин чи виробничий цех; Shopdesk — бути прив'язаним до одного або кількох підрозділів; WebSite — агрегувати залишки з набору підрозділів. Бізнес-процес передачі товару між двома магазинами і між виробничим цехом та складом готової продукції використовує ту саму фундаментальну модель місць обліку.
Альтернативою була б жорстка ієрархія типів Store, Warehouse, ProductionDepartment, але вона швидко породжує перехресні правила: магазин може одночасно бути складом, у магазині може бути кулінарія, а склад іноді є місцем прямого відвантаження інтернет-замовлень. Універсальний підрозділ краще відповідає реальній комбінації ролей.
Технічний наслідок: багато функцій Shopserver не повинні питати «це магазин чи склад?», їм достатньо DepartmentId та потрібної властивості/зв'язку. Це робить модель більш композиційною й пояснює, чому DepartmentId проходить через залишки, каси, сайти та документи як спільна координата.
4. Товар: номенклатура, а не залишок
Детальніше: Довідник товарів; Картка товару.
Товар у системі — це номенклатурна картка. Вона описує, що саме продається, закуповується, використовується у виробництві або іншим способом бере участь у бізнес-процесах. Товар має назву, одиницю виміру, категорію, штрих-коди, податкові та інші характеристики.
Важлива предметна відмінність: товар не дорівнює товарному запасу. Один і той самий товар може бути присутній одночасно в десятках підрозділів із різними залишками, цінами, доступністю та локальними налаштуваннями.
У TCU це особливо добре видно на партійному обліку. Одна номенклатура може мати декілька закупівельних партій. Кожна партія фактично походить із рядка прибуткової накладної, має власну закупівельну ціну та поточну кількість. Загальний товарний залишок на складі складається з поточних кількостей усіх таких партій.
Shopserver не переносить у фронт-офіс усю глибину цієї партійної моделі. Йому потрібна достатня оперативна інформація для продажів, доступності та обміну. Тому в його словнику окремо існує GoodsItem — номенклатурна картка — і InventoryRecord — стан цієї номенклатури в конкретному підрозділі.
У кабінеті список товарів відкривається через /Shopserver/GoodsItems. UI використовує datasource і Shopserver API, а сервер повертає proxy/DTO, сформовані з доменної моделі GoodsItem. Товар може бути пов'язаний із категорією, одиницею виміру, штрих-кодами, зображеннями та іншими властивостями.
Це розділення дає правильні відповіді на типові запитання. Якщо користувач питає «чому товар є в довіднику, але його немає в продажу на сайті», проблема може бути не в самій картці товару, а в тому, що для потрібних підрозділів немає доступного InventoryRecord, ціни, дозволу або зв'язку з каталогом конкретного сайту.
---
Архітектурний вимір: розділення номенклатури й оперативного стану
GoodsItem і InventoryRecord — приклад розділення ідентичності об'єкта та його стану в певному контексті. Номенклатурна картка змінюється відносно рідко: назва, одиниця виміру, штрих-коди, категорія, податкові ознаки. Залишок і ціна для підрозділу можуть змінюватися після кожної операції.
Якби кількість зберігалась просто в GoodsItem, система не могла б природно підтримувати декілька підрозділів. Якби для кожного магазину дублювалася вся картка товару, з'явилась би проблема синхронізації назви, штрих-кодів та інших спільних властивостей. Тому нормальна форма — один GoodsItem плюс множина InventoryRecord за підрозділами.
Це також дозволяє по-різному масштабувати потоки. Повна картка товару не потребує оновлення після кожного продажу, а InventoryRecord може бути високочастотним елементом реплікації. Саме тому в реплікаційному контейнері залишки є окремою колекцією.
Важливо, що Shopserver не намагається відтворити всю партійну історію TCU. Це оперативна проєкція, достатня для фронт-офісу. Такий підхід нагадує read model: джерело складної облікової істини залишається у back-office, а Shopserver тримає форму, оптимальну для доступності, продажів і синхронізації.
5. Категорії та групування товарів
Категорії потрібні для організації великого товарного довідника. У різних системах вони можуть мати різний практичний зміст.
В обліковому контурі групи товарів допомагають користувачу структурувати номенклатуру, будувати звіти, шукати та фільтрувати позиції. Для інтернет-магазину категорії стають ще й частиною вітрини: покупець очікує побачити ієрархію каталогу.
Тому не слід автоматично ототожнювати групу товарів у TCU з категорією зовнішнього сайту. Наприклад, інтеграція з Хорошоп передбачає, що ієрархію категорій створюють у адмін-панелі Хорошоп, потім Shopserver завантажує її до облікового запису сайту і вже після цього до категорій Хорошоп прив'язуються товари з загального довідника Base2Base.
У Shopserver є власні контрактні та доменні об'єкти категорій товарів, які використовуються кабінетом, QR Menu, сайтами та іншими функціями. На UI для товарів діють datasource-класи на кшталт GoodsCategoriesDatasource та селектори категорій.
Для мовної моделі головне правило таке: загальний товарний довідник компанії може бути значно ширшим за каталог конкретного каналу продажу. Каталог сайту — це підмножина товарів, додатково організована відповідно до структури цього сайту.
---
6. Товарний запас і InventoryRecord
Детальніше: Облік товарів у Trade Control Utility; Картка обліку товару на складі.
Товарний запас відповідає на питання: скільки певного товару є або доступно в конкретному підрозділі і за якими оперативними параметрами з ним можна працювати.
У TCU залишок є наслідком руху. Надходження збільшує запас, видаток зменшує, передача переміщує його між підрозділами, переоблік приводить обліковий стан до фактичного. При партійному обліку загальна кількість складається із залишків закупівельних партій. Система може одночасно знати FIFO/FEFO-структуру партій та середньозважену закупівельну ціну.
У Shopserver оперативне представлення такого стану — InventoryRecord. Воно пов'язує GoodsItem і Department. Саме тому на різних торгових точках один товар може мати різні кількості, ціни та ознаки доступності.
Кабінет має окрему сторінку /Shopserver/InventoryRests, де користувач бачить товарні запаси. Її datasource звертається до InventoryRecordsController, який виконує серверну фільтрацію й paging. Тобто список залишків — не локальна побудова сторінки з повного довідника товарів, а окремий серверний зріз оперативних даних.
Ця модель особливо важлива для сайтів. Якщо WebSite має прив'язаний перелік підрозділів, доступність товару для сайту формується з InventoryRecord цих підрозділів. Тому товар може існувати в загальному каталозі, але бути недоступним на конкретному сайті, якщо в жодному з дозволених підрозділів він не має доступного запасу чи потрібних параметрів.
Стаття про Хорошоп прямо демонструє це в UI: після прив'язки товарів до категорій колонки «Доступний» і «Ціна» залежать від того, чи представлений товар на підрозділах, доданих до цього сайту.
---
Архітектурний вимір: InventoryRecord як materialized operational view
З точки зору архітектури InventoryRecord варто трактувати не просто як таблицю залишків, а як матеріалізований оперативний стан, який переносить результат складнішого обліку ближче до споживачів.
У TCU залишок може бути обчислений через рух і партії, але касі або сайту недоцільно щоразу відтворювати повну історію. Фронт-офісу потрібна швидка відповідь: скільки доступно зараз, за якою ціною, у якому підрозділі. Репліка такого стану зменшує latency і залежність від back-office.
Компроміс очевидний: з'являється eventual consistency. Протягом короткого часу оперативна копія може відставати від первинного обліку. Тому система має механізми сеансів реплікації, часових міток, журналів та повторної початкової синхронізації. Це не випадкові службові деталі, а необхідна інфраструктура будь-якої системи з рознесеним станом.
Для підтримки звідси випливає метод діагностики: якщо залишок «неправильний», треба визначити, на якому рівні він неправильний — у back-office джерелі, у пакеті реплікації, в InventoryRecord Shopserver чи в локальній копії фронт-офісу.
7. Рух товару, документи й стан
Детальніше: Облік товарів у Trade Control Utility; Касовий чек, накладна та прив'язка оплат.
Base2Base описує торгову діяльність через первинні документи. Це фундаментальна предметна модель, без якої важко правильно інтерпретувати контейнери Shopserver.
Прихід товару від постачальника відображається прибутковою накладною. Продаж складається щонайменше з двох різних економічних подій: товар видається покупцю — це видаткова накладна; гроші приймаються — це прибутковий касовий ордер. Повернення покупця є зворотною складною операцією: товар повертається, а кошти віддаються. Передача між підрозділами оформлюється накладною на передачу. Зміна ціни — актом переоцінки. Списання зіпсованого чи втраченого товару — окремим документом. Переоблік — документом, який фіксує різницю між фізичним і обліковим станом.
Тобто документ — це не просто «форма вводу». Це опис факту господарської операції, який після проведення змінює стан системи.
TCU зберігає глибокий бухгалтерсько-товарний зміст цих операцій. Shopserver отримує результат та транспортне представлення, необхідне для обміну з фронт-офісом. Саме тому в його DocumentContext є сімейство Container із різними типами: чеки, Z-звіти, накладні, оплати, замовлення, виробничі та інші документи.
Для користувача кабінету сторінка контейнерів є журналом того, що реально надійшло від кас або інших клієнтів і що сталося з цими даними на сервері. Вона корисна для підтримки, діагностики інтеграцій і перевірки того, чи дійшов конкретний чек/документ до центрального контуру.
---
Архітектурний вимір: подія й стан зберігаються по-різному
У Shopserver одночасно існують два класи даних. Довідники та InventoryRecord описують поточний стан, а контейнери документів — факти, що відбулися. Це різні вимоги до зберігання.
Поточний стан можна оновлювати на місці: нова назва товару замінює стару, новий залишок стає актуальним. Бізнес-документ, навпаки, важливий як незмінна подія з ідентичністю, часом, автором, джерелом і історією доставки. Тому документи мають окремий DocumentContext, журнали завантаження/вивантаження та власний життєвий цикл.
Таке розділення наближається до принципу command/event separation, хоча Shopserver не є чистою event-sourcing системою. Він не відновлює всі довідники з історії подій; натомість використовує оптимальну модель для кожного типу даних: mutable operational state для довідників і durable transport records для документів.
8. Стаття руху та контрагент як пояснення змісту операції
У TCU кожен документ прив'язаний до певної статті руху. Стаття потрібна не лише технічно: вона класифікує економічний сенс операції й дозволяє надалі будувати звітність. Наприклад, для продажів із Prom.ua можна створити окрему статтю і потім аналізувати обсяг продажів саме цього каналу.
У Shopserver довідник статей руху представлений TransactionType і доступний у кабінеті як «Статті руху». Ці значення передаються між обліковою системою та фронт-офісом, щоб каса або інша система могла сформувати документ із правильною семантикою.
Контрагент — інша базова сутність. Це покупець, постачальник або інша сторона взаєморозрахунків. У різних інтеграціях контрагент може мати спеціальну роль. Наприклад, у сценарії Хорошоп окремі способи оплати можуть бути прив'язані до умовних контрагентів на кшталт «Хорошоп — LiqPay», тому що фактичне надходження грошей приходить від платіжної системи окремо від самого продажу.
Це пояснює, чому один продаж на сайті може створити видаткову накладну без прибуткового касового ордера: товарний рух і рух грошей — різні події та можуть завершуватися в різний час.
---
9. Shopdesk: каса як автономний фронт-офіс
Детальніше: Встановлення, перший запуск та робота з Shopdesk; Формат чека та посилання на API Shopserver.
Shopdesk — спеціалізоване робоче місце касира. Його основна задача — швидко обслуговувати покупця, а не замінювати облікову систему. Касир шукає товар, сканує штрих-код, працює з вагами, формує чек, ідентифікує покупця, застосовує знижки та бонуси, приймає оплату, друкує фіскальний або нефіскальний чек.
У статтях Base2Base підкреслюється автономність каси. Продажі не повинні зупинятись через проблеми з інтернетом. Тому Shopdesk має локальну репліку потрібних довідників і може продовжувати роботу офлайн. Центральний сервер при цьому потрібен для синхронізації, оновлення цін і залишків, передачі чеків та загального контролю.
У Shopserver кожна каса є окремим обліковим записом. У кабінеті /Shopserver/Shopdesks користувач бачить список кас, а ShopdeskEditor дозволяє редагувати конкретну касу. Реєстраційний сценарій включає генерацію коду підключення в кабінеті, після чого клієнт викликає Bridge/SignUp і передає одноразовий код разом із власним ShopdeskGuid та інформацією про пристрій.
ShopdeskGuid слід сприймати як стабільний ідентифікатор конкретного робочого місця/фронт-офісного клієнта. У статті про Shopserver прямо зазначено, що «касою» в широкому сенсі може бути не тільки фізичне робоче місце касира, а й інший фронт-енд, який генерує замовлення, продажі чи повернення.
---
Архітектурний вимір: автономність каси як вимога, що визначає протокол
Офлайн-робота Shopdesk — це не локальна оптимізація клієнта, а системна вимога. Якщо продаж повинен продовжуватись при втраті інтернету, каса не може в момент сканування кожного товару робити синхронний RPC до центральної бази.
Звідси випливає локальна репліка довідників і асинхронна доставка документів. Дані до каси надходять заздалегідь; продаж формується локально; контейнери можуть накопичуватися й відправлятися пізніше. Це store-and-forward модель.
У такій архітектурі центральний сервер має прийняти повторну доставку, відрізнити вже відомий документ від нового, вести стан обміну та не вимагати глобальної транзакції між касою, Shopserver і TCU. Саме тому GUID документів, журнали контейнерів та ідемпотентна обробка є базовими, а не допоміжними механізмами.
Реєстрація Shopdesk через одноразовий код також логічно вписується в цю модель: фізичний пристрій отримує стійку серверну ідентичність (ShopdeskGuid/обліковий запис), після чого може працювати як довгоживучий вузол реплікації, а не як випадковий HTTP-клієнт.
10. Робоча зміна каси
Каса працює не безперервним абстрактним потоком, а в межах робочих змін. Зміна об'єднує операції певного касового робочого місця та дозволяє аналізувати підсумки, чеки, X/Z-звіти й інші показники за конкретний робочий період.
Кабінет має сторінку /Shopserver/WorkShifts. Вона дає користувачу серверний погляд на зміни, що надійшли від Shopdesk. Поруч існують аналітичні сторінки підсумків за добу, порівняння й ревізії Z-звітів.
Для підтримки це важлива одиниця діагностики. Якщо користувач каже «не сходиться каса за вчора», часто треба шукати не абстрактний набір чеків, а конкретну касу, конкретну зміну, її Z-звіт та пов'язані контейнери.
---
11. Shopserver як центральний вузол реплікації
Детальніше: Shopserver. Схема обміну даними.
Основна причина існування Shopserver — розв'язати проблему взаємодії багатьох систем без жорстких прямих зв'язків між кожною парою клієнтів.
Без центрального хаба облікова система мала б окремо знати, як працювати з кожною касою, кожним сайтом, кожним терміналом. Каса мусила б мати прямий доступ до центральної облікової бази, що погано для автономності, безпеки та масштабування. Shopserver розриває ці прямі залежності.
Типова схема така:
- Облікова система формує актуальні довідники, ціни та залишки.
- Вона відправляє зміни в Shopserver у вигляді контейнера реплікації.
- Shopserver зберігає оперативний стан компанії.
- Каси та інші клієнти завантажують потрібні їм зміни.
- Каса формує продаж або інший документ локально.
- Документ надходить до Shopserver як контейнер документів.
- Облікова система періодично забирає нові контейнери й проводить їх у власній моделі.
- За потреби результат обробки повертається в Shopserver як підтвердження.
Це дві різні реплікаційні течії: довідники й оперативний стан рухаються від back-office до фронт-офісу, а первинні документи продажу та інших подій — від фронт-офісу до back-office.
---
Архітектурний вимір: два напрямки синхронізації мають різну семантику
Важливо не називати весь обмін одним словом «синхронізація». Потік довідників і потік документів мають протилежну природу.
Довідники — переважно state replication. Back-office каже: «ось актуальний стан товарів, підрозділів, цін і залишків». Shopserver застосовує зміни, а фронт-офіси отримують новішу версію стану.
Документи — event delivery. Каса каже: «ось факт продажу, який уже стався локально». Такий факт не можна просто замінити наступною версією. Його потрібно надійно доставити, зберегти, передати в облік і зафіксувати результат обробки.
Звідси різні ідентифікатори прогресу: для довідників природні LastModified/сеанси оновлення; для документів — монотонний ContainerId, DocumentGuid, журнали download/approve. Архітектура не намагається використати один універсальний реплікаційний алгоритм там, де семантика даних різна.
12. Реплікація довідників з облікової системи
Детальніше: Shopserver. Схема обміну даними.
Стаття «Shopserver — схема обміну даними» описує рекомендовану інкрементальну модель. В обліковій системі зберігається час останньої успішної реплікації. Кожен довідниковий запис має LastModified. До наступного пакета потрапляють лише записи, які змінились після попередньої реплікації.
Це важливо, тому що довідники можуть бути великими, але змінюються відносно рідко. Натомість товарні залишки змінюються часто, тому InventoryRecords є одним із найдинамічніших компонентів контейнера реплікації.
На API-рівні облікова система авторизується через AccountingSystem/Login, отримує bearer-токен і надсилає контейнер методами SendReplication або стиснутим варіантом SendReplicationZip.
У коді AccountingSystemController приймає реплікацію, а доменний шар DictionaryContext / UpdateSession застосовує зміни до довідників поточної компанії. Кабінет показує результат як «Завантаження довідників» (/Shopserver/ExchangeSessions).
Тому ExchangeSession — це не просто лог HTTP-запиту. Це бізнесово корисний журнал сеансу синхронізації, де можна побачити, чи прийшли дані від облікової системи, коли саме і з яким результатом.
---
Архітектурний вимір: інкрементальність як спосіб обмежити вартість синхронізації
Модель LastModified + UpdateSession означає, що Shopserver орієнтований на дельта-обмін, а не на постійну передачу повного каталогу. Це особливо важливо для великих товарних довідників і мереж із багатьма вузлами.
ExchangeSession зберігає не лише час початку/завершення, а й лічильники по типах сутностей, версію клієнтського застосунку, trusted device і помилки. Тобто сеанс є одночасно checkpoint, audit record та observability unit. У distributed system це дозволяє відповісти не «десь не синхронізувалось», а «у сеансі N надійшло X товарів, Y залишків і Z видалень, клієнт був такої версії».
Інкрементальна схема має класичну проблему: пропущена або пошкоджена дельта може накопичувати розходження. Саме тому існує повна початкова реплікація як механізм відновлення baseline. Це архітектурна пара: cheap incremental path + expensive repair path.
13. Чому записи не видаляються звичайною реплікацією
Стаття про обмін формулює принцип категорично: звичайна реплікація не повинна «видаляти» дані так, ніби центральний сервер є єдиним місцем зберігання. Причина в тому, що старі значення вже могли бути завантажені автономними касами й використовуватись ними.
Типовий сценарій виправлення — змінити картку в обліковій системі. Новий LastModified змусить запис увійти до наступної інкрементальної реплікації, після чого оновлення дійде до Shopserver і далі до кас.
Для повного аварійного перескладання стану існує початкова реплікація (IsInitialReplication=true). У цьому режимі Shopserver може попередньо очистити дані компанії відповідного контуру й прийняти повний комплект заново. Це ремонтний механізм, а не штатний спосіб щоденного обміну.
Це важливе знання для підтримки: коли користувач каже «я видалив товар в обліковій системі, чому він ще є на касі», відповідь не повинна зводитись до локального кешу браузера. Потрібно враховувати саму семантику реплікації й те, що фронт-офісні вузли мають власні репліки даних.
---
Архітектурний вимір: контрольоване видалення в розподіленій системі
Фізичне DELETE в центральній базі не означає, що запис зникне з автономної каси. Якщо клієнт не був онлайн у момент видалення, він навіть не дізнається про факт зміни. Тому розподілені довідники потребують окремої семантики delete — tombstone/deletion record або повної реконструкції репліки.
Наявність DeletionRecordProxy, лічильника DeletedRecordsCount у ExchangeSession та спеціального initial-replication режиму показує, що видалення розглядається як частина протоколу реплікації, а не просто як SQL-операція.
Додатково ShopserverContext.OnModelCreating примусово змінює cascade delete на Restrict для не-ownership foreign keys. Це важливий захисний сигнал: база не повинна мовчки каскадно знищувати пов'язані бізнес-дані. У системі з журналами, документами та рознесеними копіями неявний каскад часто небезпечніший за явну відмову видалення.
14. Контейнер документів: транспорт складної бізнес-операції
Детальніше: Shopserver. Схема обміну даними; Формат чека та посилання на API Shopserver.
Каса або інший фронт-офіс формує не просто одиночний рядок «продаж». Бізнес-операція може складатися з кількох пов'язаних документів. Наприклад, типовий касовий чек може містити видаткову накладну на покупця та прибутковий касовий ордер як оплату.
Для передачі такої групи документів використовується контейнер документів. У старій і поточній схемах контейнер може фізично бути zip-архівом із вмістом документів. Shopdesk зберігає сформовані файли локально, а окремий процес періодично відправляє накопичені контейнери на Shopserver. Це дозволяє касі не залежати від постійного інтернет-з'єднання в момент продажу.
На сервері DocumentContext зберігає Container та журнал пов'язаних подій. Кабінет /Shopserver/Containers/0 дозволяє переглядати ці контейнери, а редактор контейнера — бачити деталі та історію обробки.
Для RAG поняття «контейнер» треба відрізняти від «документа». Документ описує конкретну господарську подію; контейнер — транспортна й серверна оболонка, яка може містити один або декілька взаємопов'язаних документів.
---
Архітектурний вимір: контейнер — boundary object для складної операції
Контейнер вирішує одразу кілька задач. Він дає атомарну транспортну одиницю для пов'язаних документів, має власний ContainerId, файл/ім'я, тип, журнал обробки й може бути серіалізований незалежно від внутрішньої моделі back-office.
Це особливо важливо для чека, який предметно складається з декількох документів. Якби кожен документ передавався окремо, серверу довелося б відновлювати групу, вирішувати проблему частково доставленої операції та координувати порядок. Контейнер переносить межу атомарності вище: «ця бізнес-операція приїхала як ціле».
У DocumentContext базовий Container зберігається через TPH inheritance із discriminator ContainerType: ChequeContainer, ZReportContainer, PurchaseInvoiceContainer, TransferOrderContainer, ProductionContainer, CustomerOrderContainer та інші. Це дозволяє мати спільний журнал і загальний API для різних бізнес-пакетів, але зберігати спеціалізовану поведінку типів.
Такий поліморфізм проходить і через transport layer. У старій конфігурації серіалізації прямо зазначено необхідність TypeNameHandling.Objects, бо клієнту треба знати конкретний тип за базовим Document/Container. Тобто поліморфна доменна модель не приховується під десятками майже однакових endpoint-ів, а підтримується самим контрактом.
15. Отримання касових документів обліковою системою
Детальніше: Shopserver. Схема обміну даними; Касовий чек, накладна та прив'язка оплат.
Облікова система повинна періодично забирати з Shopserver нові контейнери. Рекомендована модель базується на монотонно зростаючому ContainerId.
Клієнт зберігає максимальний ID контейнера, успішно отриманого в попередньому циклі. Потім викликає AccountingSystem/DownloadContainers, передаючи fromId, максимальну кількість записів та версію свого застосунку. Після обробки нової порції запам'ятовується максимальний ID і цикл повторюється.
При імпорті обов'язково потрібна ідемпотентність. Кожен документ має DocumentGuid; якщо документ із таким GUID уже існує в обліковій базі, повторно створювати його не можна. Це захищає від дублювання при повторній доставці або повторному запуску синхронізації.
Після обробки контейнера облікова система може відправити результат через SendApproveResult. Таким чином Shopserver знає не лише факт доставки контейнера, а й результат його обробки back-office системою.
У кабінеті ланцюжок можна діагностувати через контейнери й журнали: «каса відправила» — «Shopserver прийняв» — «облікова система завантажила» — «облікова система підтвердила результат».
---
Архітектурний вимір: ідемпотентність замість distributed transaction
Між касою, Shopserver і TCU немає однієї ACID-транзакції — і це нормально. Розподілена транзакція через нестабільний інтернет і автономні клієнти була б крихкою та фактично суперечила б вимозі офлайн-продажу.
Замість цього система спирається на at-least-once delivery + idempotency. Контейнер або документ можна доставити повторно, але DocumentGuid дозволяє одержувачу визначити дубль. Прогрес читання Shopserver задається ContainerId, а результати обробки мають окремі approve/log записи.
Це важливий архітектурний компроміс: система допускає повторення транспортної операції, але не повинна допустити повторення бізнес-ефекту. Для інтеграційних систем це зазвичай надійніше, ніж спроба гарантувати «рівно один HTTP-запит».
16. Оновлення кас у реальному часі
Детальніше: Проведення чеків та оновлення даних на касі Shopdesk в реальному часі.
Архітектура поєднує дві на перший погляд суперечливі властивості: каса повинна вміти працювати автономно, але ціни та залишки бажано оновлювати максимально швидко.
Стаття про проведення чеків пояснює задум: чек надходить на Shopserver, потрапляє в обліковий контур, після зміни стану актуальні дані повертаються до фронт-офісу. У результаті каси можуть мати локальну базу для безперервної роботи й водночас швидко отримувати оновлення.
У поточному коді Shopserver для частини таких сценаріїв використовує не лише звичайний HTTP, а й SignalR. Є ShopserverSignalHub, CookdeskSignalHub, WebsiteSyncSignalHub. Це означає, що модель взаємодії не завжди є чистим polling: де потрібно, сервер може повідомляти клієнтів або UI про події та прогрес.
MassTransit використовується для асинхронних подій між сервісами. Після обробки контейнерів можуть публікуватись повідомлення, а consumers реагують незалежно від поточного HTTP-запиту. Для діагностики важливо пам'ятати: успішна відповідь контролера може бути лише початком подальшого асинхронного ланцюжка.
---
Архітектурний вимір: real-time канал не замінює durable synchronization
SignalR у Shopserver не слід сприймати як основний транспорт даних. Він використовується там, де важлива низька затримка повідомлення: Cookdesk, стан підключень, синхронізаційні сигнали для сайтів. Але факт продажу, довідник чи контейнер не повинні існувати лише як transient SignalR message.
Це розділення ролей дуже здорове: durable state/event зберігається в SQL і передається через API/контейнери; notification може летіти через SignalR. Якщо клієнт пропустив сигнал, він повинен мати можливість відновити стан із сервера.
MassTransit вирішує іншу задачу — асинхронне розв'язування між серверними сервісами. Після ApproveContainers Shopserver публікує ContainersUpdatedMessage; consumers на інших сервісах можуть реагувати незалежно від HTTP-клієнта. Тобто в системі є три різні комунікаційні режими: request/response HTTP, transient push через SignalR і durable/asynchronous service events через message bus. Вибір залежить від семантики, а не від технологічної моди.
17. Кабінет Base2Base: не окрема база, а інтерфейс до сервісів
AndriyCo.Site2023 — це веб-сайт і кабінет користувача. Він не є монолітною копією всіх доменних даних Base2Base. Для Shopserver сайт виступає клієнтом API.
Це видно навіть на рівні залежностей: AndriyCo.Site2023 має ProjectReference на AndriyCo.Shopserver.Proxy, але не на AndriyCo.Shopserver.Data. Тобто UI знає контракти передачі даних, але не повинен знати внутрішні EF Core-сутності Shopserver.
Типовий шлях виглядає так:
Razor page → Datasource → ShopserverChannel → HTTP API → Controller → DictionaryContext/DocumentContext → domain model → Proxy/DTO → UI.
Цей патерн повторюється майже в усіх сторінках Pages/Shopserver. Datasource інкапсулює роботу з endpoint-ами, а сторінка зосереджується на відображенні, фільтрах, діалогах та діях користувача.
Тому коли користувач каже «в кабінеті не відображається залишок», причинний ланцюжок слід шукати не тільки в Razor-компоненті. Дані могли не потрапити в Shopserver з облікової системи, могли бути прив'язані до іншого підрозділу, endpoint міг повернути порожній результат через фільтр, або UI міг застосувати власний відбір.
---
Архітектурний вимір: кабінет як API client, а не «друга версія Shopserver»
Межа залежностей дуже показова. AndriyCo.Site2023.csproj посилається на AndriyCo.Shopserver.Proxy, але не на AndriyCo.Shopserver.Data. Це означає, що веб-кабінет компіляційно не має доступу до EF entities та внутрішніх серверних методів.
Архітектурний сенс — contract-first boundary. UI може знати GoodsItemProxy, ShopdeskProxy, InventoryRecordProxy, але не повинен знати, у скількох таблицях і через які navigation properties це зберігається. Сервер може змінити persistence model, не змушуючи UI стати частиною тієї ж транзакційної моделі.
AndriyCo.Shopserver.Proxy таргетує netstandard2.0, тоді як сервер і сайт — сучасні .NET. Це сильний сигнал, що контрактна бібліотека задумана для ширшого кола клієнтів і довшої сумісності, включно зі старішими застосунками. Proxy виступає anti-corruption layer між різними технологічними поколіннями Base2Base.
Datasource-шар сайту додає ще одну межу: Razor-компоненти не розкидають HTTP-виклики по markup/code-behind, а працюють через спеціалізовані джерела даних. Це полегшує повторне використання пагінації, фільтрації, завантаження та централізованої роботи з каналом Shopserver.
18. Меню Shopserver у кабінеті як карта системи
Після авторизації WebSiteLayout.razor показує окрему групу Shopserver. Вона фактично є хорошою предметною картою поточної реалізації.
У ній є:
- меню швидкого доступу;
- QR Menu;
- Web-сайти;
- довідник товарів;
- товарні запаси;
- стоп-лист;
- каси Shopdesk;
- термінали збору даних;
- касири;
- статті руху;
- вагові сервери;
- налаштування;
- робочі зміни;
- контейнери документів;
- завантаження довідників;
- аналітичні сторінки та ревізія Z-звітів.
Ця структура корисніша для людського опису, ніж перелік контролерів, тому що приблизно відповідає тому, як користувач думає про систему. Людина шукає «каси», «товари», «залишки», «сайти», «чеки», а не ShopdeskController, GoodsItemsController чи ContainersController.
Технічні назви потрібні вже після визначення області. Наприклад, «Каси Shopdesk» → Shopdesks.razor → ShopdeskListDatasource → ShopdeskController → доменний Shopdesk.
---
19. Довідник кас Shopdesk у кабінеті
Сторінка /Shopserver/Shopdesks показує зареєстровані касові робочі місця компанії. Це не просто список комп'ютерів. Кожен запис описує окремого фронт-офісного учасника обміну, який може авторизуватись, отримувати довідники й надсилати документи.
Картка каси використовується для налаштування й контролю. Реєстраційний код, GUID пристрою, підрозділ, активність та інші параметри дозволяють Shopserver розуміти, хто саме звертається до нього й яку частину даних цьому клієнту треба віддати.
UI реалізований через ShopdeskListDatasource і ShopdeskEditorDataSource, API — через ShopdeskController і BridgeController. BridgeController є особливо важливим для runtime-клієнта: через нього відбуваються реєстрація/авторизація та частина обміну між касою і Shopserver.
Для підтримки варто розділяти дві різні проблеми: каса не зареєстрована/не авторизується і каса зареєстрована, але має проблеми з реплікацією або документами. У першому випадку ключовими є обліковий запис і sign-up, у другому — сеанси обміну, контейнери, журнали та актуальність даних.
---
20. Касири та права
Касир — це людина, яка працює на Shopdesk. Довідник касирів реплікується до фронт-офісу, щоб каса могла ідентифікувати оператора і застосувати його права.
У статтях Shopdesk є окремі теми про права касира, секретний код/штрих-код, адміністративні функції. Це підкреслює, що каса не повинна давати будь-якому оператору однаковий доступ до повернень, службових внесень/винесень, налаштувань чи інших чутливих операцій.
Shopserver зберігає оперативний довідник Cashier, а кабінет показує його на /Shopserver/Cashiers. API реалізований CashiersController.
Касир не тотожний веб-користувачу кабінету. Веб-користувач авторизується через серверний контур Base2Base і керує компанією/сервісами; касир — учасник операцій на конкретній касі.
---
21. Data Terminal та складські операції
Data Terminal — мобільний або спеціалізований клієнт для операцій, де сканування товару зручніше виконувати безпосередньо біля полиці або складу. Типові задачі — переоблік, приймання товарів, збір даних.
Предметно він працює з тими самими товарами, штрих-кодами й підрозділами, що й інші системи, але має інший сценарій взаємодії з оператором.
У кабінеті є /Shopserver/DataTerminals та редактор Data Terminal. Дані обслуговуються DataTerminalController і відповідними datasource-класами. Сам факт наявності окремого облікового запису для термінала показує загальний архітектурний принцип Shopserver: різні фронт-офісні пристрої централізовано реєструються, отримують необхідний зріз корпоративних довідників і працюють від імені конкретної компанії.
---
22. Scale Server і ваги з друком етикеток
Детальніше: Ваги; Вагові штрих-коди.
Ваги з друком етикеток мають власну локальну базу товарів, цін і вагових кодів. Різні виробники використовують різні протоколи, тому Base2Base виділяє інтеграцію в окремий компонент Scale Server.
З погляду користувача задача проста: актуальний перелік вагових товарів і цін повинен потрапити у ваги незалежно від моделі конкретного обладнання.
Shopserver зберігає конфігурацію вагових серверів, а кабінет має /Shopserver/ScaleServers і відповідний редактор. На сервері є ScaleServerController та ScaleServerApiController: один контур більше орієнтований на управління, інший — на runtime-взаємодію.
Ця область є прикладом того, як Shopserver уніфікує інтеграцію: облікова система не повинна напряму знати протокол кожних фізичних ваг.
---
23. Quick Menu, QR Menu та стоп-лист
Детальніше: Безкоштовне QR Меню за 5 хвилин; Запуск QR Меню. Запитання та відповіді.
Quick Menu вирішує задачу швидкого доступу до товарів на касі або іншому фронт-офісному інтерфейсі. Замість пошуку по великому довіднику користувач отримує структуроване меню популярних або спеціально відібраних позицій.
QR Menu використовує схожу ідею каталогу, але вже для покупця. Відвідувач сканує QR-код, відкриває меню, формує замовлення. У кафе та фастфуді це дозволяє розпаралелити точку приймання замовлень і зменшити навантаження на касира.
Стоп-лист вирішує зворотну задачу: тимчасово приховати або заборонити продаж позиції, яка формально є в каталозі, але зараз недоступна з операційних причин.
У Shopserver ці функції мають окремі доменні моделі, контролери й сторінки кабінету: QuickMenuController, QrMenuController, StopListController; /Shopserver/QuickMenus, /Shopserver/QrMenus, /Shopserver/Stoplist.
QR Menu може використовувати залишки конкретного підрозділу, тому знову проявляється зв'язок «товар → підрозділ → InventoryRecord → доступність у каналі продажу».
---
24. WebSite як універсальний канал продажу
У Shopserver веб-сайт не слід розуміти лише як URL. Це обліковий запис окремого каналу продажу, який має власні налаштування, каталог, підрозділи, правила доступності й інтеграційні параметри.
У загальному довіднику компанії можуть бути тисячі товарів, але конкретному сайту потрібна лише певна підмножина. Так само компанія може мати десятки підрозділів, а сайт продає тільки зі складів або магазинів, які до нього прив'язані.
Тому модель WebSite зв'язує принаймні три речі:
- ідентичність каналу — який саме сайт/магазин налаштовується;
- каталог — які товари повинні потрапити в цей канал і як вони згруповані;
- джерела залишків і цін — які підрозділи беруть участь у формуванні доступності.
У кабінеті це /Shopserver/WebSites, редактори конкретних типів сайтів і WebSiteDatasource. На сервері — WebSitesController та доменна модель WebSite з конкретними різновидами.
Така модель дозволяє мати декілька сайтів для однієї компанії, навіть якщо вони продають різний асортимент із різних підрозділів.
---
Архітектурний вимір: WebSite як plugin-like integration boundary
Різні e-commerce платформи мають різні API, моделі категорій, статуси замовлень, правила залишків і ціноутворення. Якщо ці відмінності проникнуть у загальну модель Shopserver, кожна нова інтеграція збільшуватиме кількість if (siteType == ...) у всіх шарах.
Тому правильна межа — спільне поняття WebSite для того, що однаково для всіх каналів, і спеціалізована реалізація для конкретного провайдера. Користувач у кабінеті бачить «сайт» як канал продажу, а серверна інтеграція знає деталі Хорошоп, власного OnlineStore тощо.
Це схоже на plugin/strategy architecture: ядро визначає стабільні поняття — обліковий запис сайту, товари, категорії, підрозділи-джерела, замовлення, стани синхронізації; адаптер платформи реалізує конкретний wire protocol. Така структура знижує ризик того, що зміна API Хорошоп вплине на касовий контур або на інший сайт.
25. Хорошоп: приклад повного e-commerce контуру
Детальніше: Інтеграція з ХОРОШОП.
Інтеграція з Хорошоп добре демонструє предметну роль Shopserver. Вона побудована не як прямий міст TCU ↔ Хорошоп, а як облікова система ↔ Shopserver ↔ Хорошоп.
Це дозволяє залишити Shopserver єдиним інтеграційним хабом для кас, сайтів, облікових систем та інших сервісів.
Схема складається з декількох рівнів.
Категорії. Ієрархія груп створюється на боці Хорошоп, тому що платформа має власну модель сторінок і категорій. Shopserver завантажує цю структуру і дозволяє прив'язати до неї товари Base2Base.
Каталог. До конкретного сайту додається лише потрібна частина загального товарного довідника.
Підрозділи. Користувач задає магазини/склади, з яких сайт може продавати. Вони визначають, які InventoryRecord беруть участь у розрахунку доступності та ціни.
Вивантаження. Активний сайт циклічно синхронізує нові або змінені товари, ціни та доступність. У коді цим займаються HoroshopSyncBackgroundService і processors у WebSiteIntegrations/Horoshop.
Продажі. Shopserver отримує замовлення/продажі з Хорошоп і формує контейнери документів для подальшого імпорту в облікову систему.
Оплати. Продаж товару і надходження грошей не завжди відбуваються одним документом. Для LiqPay, NovaPay та інших посередників оплату треба трактувати відповідно до реального фінансового потоку. Тому інтеграція може створювати видаткову накладну на умовного контрагента, пов'язаного зі способом оплати, а фактичне надходження грошей проводиться окремо пізніше.
У кабінеті є HoroshopSiteEditor.razor. На сервері — загальний WebSitesController для конфігурації та окремий шар WebSiteIntegrations/Horoshop для фактичного обміну.
---
Архітектурний вимір: зовнішній сайт не є джерелом облікової істини
Інтеграція з Хорошоп показує принцип ownership. Хорошоп є зовнішнім каналом продажу й вітриною, але базові товарні дані, дозволені залишки та бізнес-рішення походять із контуру Base2Base. Водночас частина сутностей, наприклад зовнішня категорійна структура або external IDs, належить самому сайту й повинна бути зіставлена з внутрішньою моделлю.
Тому інтеграція неминуче має mapping layer. Це не «зайва таблиця зв'язків», а спосіб не змішувати ідентичність товару Base2Base з ідентичністю об'єкта в сторонній платформі. Саме така межа дозволяє повторно експортувати каталог, переживати зміну назв і не використовувати зовнішній ID як primary key внутрішньої номенклатури.
Замовлення рухаються у протилежний бік і теж проходять адаптацію: зовнішній order потрібно перетворити на внутрішній документ/контейнер із зрозумілою семантикою. Таким чином інтеграція є двонаправленим translation layer, а не просто HTTP proxy.
26. Власний OnlineStore
Внутрішній AndriyCo.Shopserver.OnlineStore — ще один канал продажу, але він контрольований самою екосистемою Base2Base. Для нього Shopserver формує спрощений snapshot каталогу, який не розкриває runtime-вітрині всю складну внутрішню модель DictionaryContext.
Це важливий архітектурний принцип: зовнішньому клієнту потрібно віддати рівно ту модель, яка потрібна для його задачі, а не змушувати його розуміти внутрішню EF-структуру Shopserver.
OnlineStoreExportController віддає snapshot і зображення. Адміністративні налаштування сайту зберігаються через WebSite-модель і редагуються в кабінеті OnlineStoreSiteEditor.
Предметно OnlineStore використовує ті самі базові поняття: загальний товарний довідник, вибраний каталог сайту, перелік підрозділів, доступність, ціна, зображення та замовлення покупця.
---
27. TCC і програма лояльності
Детальніше: Архітектура сервісів Base2Base; Маркетингова акція програми лояльності; Керування бонусами у Trade Control Center.
Trade Control Center — окремий сервісний контур. Його основний предмет — покупець, CRM, бонусний баланс, маркетингові акції та лояльність.
Shopdesk інтегрується з TCC під час роботи з покупцем. Покупець може бути знайдений або зареєстрований, до чека застосовуються правила програми лояльності, нараховуються або списуються бонуси.
Shopserver має точки інтеграції з TCC (TccController, пов'язані event consumers, налаштування компанії). У кабінеті налаштування Shopserver містять параметри TCC та франчайзингового контуру.
Важливо не змішувати сутності. Контрагент у товарно-фінансовому обліку і CRM-покупець можуть перетинатися за сенсом, але це не обов'язково одна й та сама технічна модель. У відповідях слід дивитися на конкретний сценарій: продаж у TCU, ідентифікація покупця на касі, бонусна операція TCC чи e-commerce замовлення.
---
Архітектурний вимір: TCC — окремий bounded context
Shopserver не намагається володіти повною CRM-моделлю покупця, бонусів і маркетингових акцій. Ці функції належать TCC. Це приклад розподілу системи на bounded contexts: товарно-оперативний контур і CRM-контур взаємодіють, але мають різні джерела істини.
Подієва інтеграція через MassTransit тут особливо доречна. Наприклад, після появи контейнера або зміни користувача інший сервіс може реагувати асинхронно. HTTP-клієнту, який щойно відправив чек, не потрібно чекати завершення всіх CRM-процесів. Це зменшує зв'язування latency і failure modes між сервісами.
Такий поділ пояснює важливу річ для підтримки: якщо чек є в Shopserver, це ще не означає, що кожен похідний CRM-ефект уже завершився. Треба розрізняти приймання первинної події та її асинхронну обробку іншими bounded contexts.
28. Налаштування компанії в Shopserver
Сторінка /Shopserver/Settings — це кабінетна картка поточної компанії в контексті Shopserver. Вона працює через SettingsDataSource і CompanyController.
Окрім загальних параметрів, тут є інтеграційні налаштування TCC, франчайзингові ідентифікатори, URL сервісів, облікові дані та інші параметри, які визначають поведінку конкретного tenant-а.
Тому «налаштування Shopserver» — не глобальна конфігурація всього сервера. Значна частина параметрів належить конкретній компанії і впливає лише на її контур.
---
29. DictionaryContext і DocumentContext: технічне відображення двох різних світів
У коді Shopserver є щонайменше два великі EF Core-контексти.
DictionaryContext зберігає оперативний стан: компанії, користувачів, товари, категорії, одиниці виміру, підрозділи, контрагентів, товарні записи, касирів, статті руху, Shopdesk, Data Terminal, Scale Server, WebSites, меню, зображення та іншу конфігурацію.
DocumentContext зберігає контейнери документів і журнали їхнього життєвого циклу.
Це розділення добре відповідає предметній моделі:
- «що зараз відомо системі про компанію, товари, залишки й пристрої» — словниковий/оперативний світ;
- «які бізнес-події прийшли від клієнтів, хто їх завантажив, як вони були оброблені» — документальний/журнальний світ.
Для діагностики корисно визначити, до якого світу належить проблема. Якщо невірна ціна на касі — треба дивитись реплікацію та InventoryRecord. Якщо пропав чек — треба дивитись Container, логи доставки та підтвердження.
---
Архітектурний вимір: дві бази/контексти — різні профілі навантаження й життєвий цикл
DictionaryContext і DocumentContext мають різні connection string і окремо реєструються як EF Core contexts. Це сильніше рішення, ніж просто дві групи таблиць в одному DbContext.
Довідники — переважно поточний стан із частими upsert під час реплікації та великою кількістю читань від клієнтів. Документи — append-oriented історія подій, яка росте безперервно, потребує журналів доставки, retention/очищення старих даних і може мати інший профіль індексів та backup.
Розділення дозволяє незалежніше керувати цими двома типами навантаження. У solution навіть є OldDataCleaner, що логічно стосується довгоживучого історичного шару. DictionaryContext.CreateDocumentContext() явно показує, що домен довідників іноді відкриває документну базу, але не зливає їх в одну persistence boundary.
Компроміс — немає простої локальної EF-транзакції, яка атомарно охоплює обидві бази. Тому архітектура повинна приймати eventual consistency між довідниковим і документним станом. Це узгоджується з загальною distributed моделлю Shopserver.
30. Proxy/DTO як контракт між сервісом і клієнтом
AndriyCo.Shopserver.Proxy — критично важлива межа архітектури. Це не просто «папка DTO». Вона дозволяє сайту, касам та іншим клієнтам працювати зі стабільними контрактами, не залежачи від внутрішньої структури бази Shopserver.
Наприклад, GoodsItemProxy описує номенклатурну картку для передачі, але не зобов'язує клієнта знати всі навігаційні зв'язки EF. SiteInventoryRecordProxy дає UI готове представлення товарного запасу. WebSite DTO передають лише потрібні дані конфігурації каналу.
Через це при аналізі коду не можна автоматично трактувати DTO як повну доменну модель. DTO — це проєкція моделі для конкретного контракту.
Для RAG технічні назви DTO корисні як точні маркери, але людське пояснення має спиратися на поняття «товар», «залишок», «каса», «сайт», «контейнер», а не на перелік властивостей класу.
---
Архітектурний вимір: Proxy/DTO захищає протокол від persistence model
У AndriyCo.Shopserver.Proxy знаходяться не лише прості DTO, а й поліморфні документи, фільтри, response-моделі, authorization structures, дерева та службові контракти. Це фактично публічна мова Shopserver для клієнтів.
Внутрішня entity може мати navigation properties, EF-атрибути, технічні ключі й поведінку. Proxy повинен містити лише те, що має сенс за межами сервера. Методи ToProxy/FromProxy виконують явне перетворення замість автоматичного exposing entities.
Це рішення має ще одну історичну перевагу: netstandard2.0 дає змогу використовувати контракт у клієнтах, які не можуть перейти на той самий target framework, що й сервер. Для екосистеми, де існують старі касові та desktop компоненти, стабільний контракт важливіший за зручність прямого reference на Data.
Недолік — дублювання класів і mapping-коду. Але це свідомо сплачена ціна за versioning boundary, безпеку серіалізації та технологічну незалежність клієнтів.
31. Авторизація кабінету й авторизація Shopserver
Веб-користувач спочатку авторизується в контурі сайту/Identity/Billing. Сайт отримує bearer-токен та claims. Для виклику Shopserver ShopserverChannel передає цей токен у HTTP Authorization header.
На Shopserver middleware перевіряє JWT, а базовий контролер додатково відновлює доменний контекст користувача. DictionaryContext.Authorize встановлює CurrentCompany, після чого запит працює в межах конкретного tenant-а.
Це два послідовні шари, а не одна локальна змінна на Razor-сторінці. Тому UI може знати CompanyGuid, але остаточне право доступу до даних визначається сервером.
Для SignalR у Program.cs передбачена передача access token через query string для hub-з'єднань, що є стандартним технічним винятком для websocket/SignalR-клієнтів.
---
Архітектурний вимір: повторна авторизація на сервісній межі
Сайт уже знає, хто користувач, але Shopserver не довіряє самому факту, що запит прийшов із сайту. Кожен захищений API-запит має власний bearer identity, з якого Shopserver створює LoginData і виконує DictionaryContext.Authorize.
Це принцип zero implicit trust between application boundaries: веб-сайт є клієнтом, а не привілейованим внутрішнім шаром, який може передати довільний CompanyId. Така модель також дозволяє тим самим API користуватися не тільки сайту.
На практиці це додає повторну роботу з claims/користувачем, але прибирає небезпечну залежність від session state одного конкретного frontend. У distributed topology це значно надійніше.
32. Журнали й спостережуваність
Розподілена система без журналів майже неможлива в підтримці. Shopserver зберігає декілька рівнів спостережуваності.
Exchange Sessions показують сеанси завантаження/оновлення довідників.
Containers показують первинні бізнес-пакети, які надійшли від кас, сайтів та інших клієнтів.
LogRecords фіксують події життєвого циклу контейнерів: завантаження, скачування, підтвердження, видалення та інші результати.
Work shifts і Z-revision дають операційний зріз касової роботи.
Веб-логи сервісу доповнюють це технічними exception та інформаційними повідомленнями.
Тому під час діагностики питання «де загубився документ?» треба розкладати на кроки й шукати останню підтверджену точку ланцюжка.
---
Архітектурний вимір: observability є частиною бізнес-протоколу
У Shopserver журнали присутні на двох рівнях. Звичайне application logging відповідає на питання про виконання коду, elapsed time, exception, remote IP тощо. Але для обміну цього недостатньо: потрібні бізнесові журнали доставки — UploadContainerRecord, DownloadContainerRecord, ApproveResultContainerRecord, ExchangeSession.
Різниця принципова. Application log може бути ротований або загублений і не є доменною істориєю. Запис «цей контейнер був завантажений TCU такого-то числа і отримав такий результат» повинен бути частиною даних системи, бо це основа підтримки й відновлення ланцюжка.
У ShopserverController.GetObjectResult додатково фіксуються кількість повернутих рядків, remote endpoint і elapsed. В ApproveContainers окремо попереджається про повільну обробку контейнера. Тобто спостережуваність закладена не лише на рівні IIS, а в самих ключових операціях.
33. Аналітичні сторінки кабінету
Shopserver збирає достатньо оперативних даних, щоб показувати користувачу підсумки продажів і роботи кас без необхідності відкривати TCU.
У кабінеті є DayBottomLine, DayBottomLineCompare, PeriodChart, ZRevision. InfoController формує агреговані дані по компанії або вибраних підрозділах.
Це оперативна аналітика, а не повна бухгалтерська звітність. Для глибокого аналізу собівартості, партій, взаєморозрахунків та первинних документів основним джерелом залишається облікова система.
Таке розділення важливе: Shopserver може швидко відповісти на питання про стан фронт-офісу, але не повинен дублювати весь back-office функціонал TCU.
---
33A. Наскрізні архітектурні рішення Shopserver
Цей розділ збирає рішення, які не належать одній функціональній області, але визначають поведінку системи в цілому.
33A.1. HTTP, SignalR і MassTransit — три різні гарантії доставки
Shopserver використовує кілька транспортів не тому, що вони взаємозамінні, а тому, що кожен відповідає іншій семантиці.
HTTP API використовується, коли клієнту потрібна явна request/response операція: авторизуватись, отримати список, передати пакет, зберегти налаштування. Клієнт знає результат конкретного запиту.
SignalR використовується як low-latency notification channel. Хаби ShopserverSignalHub, CookdeskSignalHub, WebsiteSyncSignalHub дозволяють повідомити активний клієнт без частого polling. Але SignalR connection може розірватись, тому критичний стан не повинен існувати лише в повідомленні хаба.
MassTransit використовується для service-to-service events. В ApproveContainers після збереження контейнерів формується ContainersUpdatedMessage і публікується через IPublishEndpoint. Consumers (ApproveResultFromTccEventConsumer, UserUpdatedEventConsumer та інші) мають свої endpoint-и. Це дає fan-out: одна подія може мати декілька незалежних реакцій без прямого виклику всіх сервісів відправником.
Отже архітектурне правило можна сформулювати так: HTTP змінює/читає authoritative state, SignalR пришвидшує реакцію клієнта, message bus розв'язує серверні процеси в часі та по залежностях.
33A.2. Локальна SQL-транзакція там, де вона реально можлива
У ApproveContainers кожен файл контейнера обробляється у власній транзакції DocumentContext.Database.BeginTransactionAsync. Це хороша межа атомарності: конкретний контейнер або зберігся коректно, або його локальні зміни відкочуються.
При цьому публікація ContainersUpdatedMessage відбувається після commit. Система не намагається включити RabbitMQ і SQL Server в одну distributed transaction. Це означає, що між commit і publish теоретично існує failure window. У коді немає повноцінного transactional outbox, тому це слід враховувати як поточну архітектурну властивість, а не приховувати.
Для RAG це важливо: коли пояснюється «контейнер збережено, але похідна подія не відпрацювала», така ситуація принципово можлива в асинхронній архітектурі й діагностується по SQL-журналу та message-bus логах окремо.
33A.3. Відмова від cascade delete як захист історії
ShopserverContext.OnModelCreating проходить усі foreign keys і змінює DeleteBehavior.Cascade на Restrict, крім ownership. Це глобальне правило persistence layer.
Причина добре випливає зі специфіки системи: довідник може бути пов'язаний із документами, журналами та автономними репліками. Автоматичне каскадне видалення одного root entity здатне непомітно знищити історичні дані. Restrict змушує видалення бути явним і контрольованим.
Це поєднується з deletion records та початковою реплікацією: «видалити з SQL» і «прибрати з усієї розподіленої системи» — різні операції.
33A.4. Поліморфізм документів як частина доменної моделі
DocumentContext використовує TPH discriminator для Container і LogRecord. Це означає, що різні типи документних контейнерів мають спільний base type і спільні механізми журналу/доставки, але можуть мати спеціалізовані поля та поведінку.
Старі коментарі конфігурації Newtonsoft прямо пояснюють, навіщо в JSON передавати type metadata: базовий Document абстрактний, а клієнт повинен десеріалізувати конкретний SalesInvoice, PurchaseInvoice тощо. Тобто серіалізаційна політика продиктована не framework preference, а доменним поліморфізмом.
33A.5. Lazy loading вимкнено свідомо на рівні context configuration
І DictionaryContext, і DocumentContext конфігуруються з UseLazyLoadingProxies(false). Це робить завантаження graph-а явним: у коді видно Load... методи та LINQ projection у Proxy.
Для API це важлива властивість. Lazy loading легко породжує N+1 queries і випадкові звернення до БД під час JSON serialization. Явне завантаження дає передбачуваніший SQL і чіткішу межу того, що входить у відповідь.
33A.6. Версія клієнта є частиною протоколу
У ExchangeSession зберігається AppVersion, а download/send методи також передають інформацію про версію клієнтського ПЗ. У довгоживучій екосистемі з касами та desktop clients це важливо для backward compatibility й діагностики.
Коли помилка виникає лише на частині кас, питання «яка версія клієнта?» є не загальним troubleshooting advice, а даними, передбаченими самим протоколом обміну.
33A.7. Background services відокремлюють housekeeping від request path
У Shopserver присутні GoodsCatalogReplicationService, IdentityServerSynchonization, OldDataCleaner, CrmFileService. Такі процеси не повинні виконуватися всередині випадкового користувацького HTTP-запиту: вони мають власний lifecycle, можуть працювати за розкладом/циклом і продовжувати роботу без активного UI.
Це ще один прояв розділення synchronous user path та eventual background work. Сервіс може швидко відповісти клієнту, а синхронізація допоміжної системи або очищення історії відбудеться окремо.
33A.8. API не дорівнює домену
Контролери Shopserver тонкі в тому сенсі, що суттєва поведінка розміщена в data/domain classes (Container.Approve, GoodsItem, Shopdesk, UpdateSession тощо). Це зменшує ризик дублювання правил між різними endpoint-ами.
Водночас domain layer не є «чистим DDD ядром»: сутності активно знають DictionaryContext, EF Core та Proxy mapping. Тобто архітектура прагматична, а не академічно layered. Її сильна сторона — концентрація бізнесових перевірок біля сутностей і контексту; слабша — високий coupling до persistence. Для опису системи краще називати це фактичним рішенням, а не підганяти код під ідеальний патерн.
---
34. Товарний облік TCU як контекст для правильного розуміння Shopserver
Детальніше: Облік товарів у Trade Control Utility; Картка обліку товару на складі.
Статті про TCU потрібні тут не для того, щоб приписати Shopserver чужу функціональність, а щоб пояснити походження даних.
TCU використовує універсальну модель товарного обліку. Закупівельна партія не створюється як окрема абстрактна сутність: фактично це рядок прибуткової накладної з початковою та поточною кількістю і закупівельною ціною. Відвантаження вибирає партії за FIFO або FEFO, система одночасно може розрахувати середньозважену закупівельну ціну.
Рух зберігається в журналі операцій, що дозволяє обчислювати залишок на будь-яку дату й робити чистий відкат документів. Стан InventoryRecords у TCU є розумно достатньою заготівлею поточного результату, але первинна істина походить із рухів і документів.
Коли ці дані реплікуються до Shopserver, фронт-офісу передається не вся історія партій і проводок, а актуальний зріз, потрібний для роботи. Саме тому питання «звідки взявся залишок 12 шт. у Shopserver» у кінцевому підсумку може вимагати аналізу документів і рухів в обліковій системі, хоча в самому Shopserver видно лише поточний InventoryRecord і історію реплікації.
---
35. Продаж як приклад наскрізного сценарію
Детальніше: Касовий чек, накладна та прив'язка оплат; Видаткова накладна; Прибутковий касовий ордер.
Розглянемо типовий продаж у магазині.
- В обліковій системі існує товар і товарний запас на підрозділі-магазині.
- Актуальний товар, ціна і залишок реплікуються в Shopserver.
- Shopdesk цієї торгової точки завантажує актуальні дані й зберігає їх у локальній репліці.
- Касир сканує товар і формує чек.
- Shopdesk створює документи продажу. Предметно продаж товару — це видаткова накладна; приймання грошей — окрема операція оплати.
- Пов'язані документи упаковуються в контейнер і зберігаються локально.
- Фоновий процес Shopdesk відправляє контейнер у Shopserver, коли є зв'язок.
- Shopserver приймає контейнер, журналізує його й робить доступним для подальшого імпорту.
- Облікова система завантажує новий контейнер, перевіряє
DocumentGuidна ідемпотентність і створює/проводить первинні документи у своїй моделі. - Проведення змінює товарний стан у TCU.
- Нові залишки потрапляють до наступної реплікації в Shopserver.
- Каси отримують оновлені залишки.
Цей сценарій пояснює, чому коротка затримка між продажем і зміною центрального залишку є природною властивістю розподіленої системи, хоча сучасна реалізація намагається максимально скоротити цикл і частково використовує події/SignalR.
---
36. Передача товару між магазинами
Детальніше: Облік товарів у Trade Control Utility.
Якщо компанія має два магазини, створювати два незалежні товарні довідники не потрібно. Один товар може мати запас у двох різних підрозділах.
Коли на одному магазині товар закінчується, а на другому є надлишок, TCU оформлює накладну на передачу. Вона зменшує запас на підрозділі-відправнику і збільшує на підрозділі-отримувачі.
Після проведення новий стан InventoryRecord для обох підрозділів реплікується в Shopserver. Каси цих магазинів та пов'язані сайти отримують уже змінені доступні кількості.
Тому Shopserver не «переміщує товар» сам по собі. Він відображає оперативний результат операції, яка має первинний обліковий сенс у back-office.
---
37. Переоблік і розбіжності залишків
Детальніше: Облік товарів у Trade Control Utility; Картка обліку товару на складі.
Переоблік потрібен, щоб привести обліковий стан до фізичної реальності. Розбіжності можуть виникати через крадіжки, помилки приймання, пересорт, псування, помилки попередніх інвентаризацій.
У TCU акт переобліку створює коригуючі рухи. Після його проведення змінюються товарні запаси підрозділу. Далі ці зміни потрапляють у звичайний канал реплікації до Shopserver і на каси.
Якщо в кабінеті Shopserver після переобліку все ще видно старий залишок, логічна діагностика така:
- чи проведений документ у TCU;
- чи змінився фактичний
InventoryRecordу back-office; - чи сформована та успішно відправлена реплікація;
- чи є успішний ExchangeSession у Shopserver;
- чи оновився
InventoryRecordпотрібногоDepartmentId; - чи отримала конкретна каса/сайт нову версію даних.
Це краща модель мислення, ніж перевіряти лише сторінку браузера.
---
38. Ціна товару та канал продажу
Детальніше: Робота з цінами у Trade Control Utility.
Ціна в торговій системі не є просто однією властивістю GoodsItem. Вона пов'язана з товарним станом, підрозділом, правилами ціноутворення, переоцінками та конкретним каналом продажу.
У TCU зміна ціни документується переоцінкою, а середньозважена закупівельна ціна формується з поточних партій. Ціна продажу може відрізнятись від закупівельної й бути предметом окремих правил, знижок та маркетингових механізмів.
Shopserver передає фронт-офісу вже оперативну ціну, достатню для продажу. Сайт при цьому може агрегувати або обирати ціну з підрозділів, прив'язаних до конкретного WebSite.
Тому питання «чому на сайті інша ціна, ніж у картці товару» треба розглядати через джерело ціни конкретного каналу, підрозділи сайту, реплікацію та правила інтеграції, а не тільки через GoodsItem.
---
39. Виробництво як приклад складнішого контексту
Детальніше: Технологічна карта страви та облік виробництва для ресторанів та кафе.
Виробництво в TCU показує, наскільки глибокою може бути предметна модель за простим товарним залишком.
Технологічна карта описує склад виробу, норми інгредієнтів, вихід і технологію. Виробничий акт списує інгредієнти та оприбутковує готовий виріб. Напівфабрикати можуть бути вкладеними на багато рівнів. FIFO визначає фактичну собівартість використаних інгредієнтів.
У «тягнучій» схемі нестача готового виробу під час видатку може автоматично створити виробничий акт і довиготовити потрібну кількість. Частина напівфабрикатів може вже бути на складі, а відсутня частина — автоматично виготовитись.
Для Shopserver важливий результат: коли виробництво змінює залишки готових виробів та інгредієнтів, оновлені InventoryRecord реплікуються фронт-офісу. Каса не повинна сама відтворювати рекурсивний алгоритм виробництва, щоб продати готовий товар.
---
Архітектурний вимір: Shopserver зберігає проєкцію виробництва, а не переносить виробничий engine
Статті про виробництво дуже багаті предметно, але це хороший приклад межі відповідальності. Рецептури, FIFO-витрата інгредієнтів, рекурсивне довиробництво та розрахунок фактичної собівартості — це back-office логіка TCU. Shopserver може транспортувати відповідні товари, залишки, документи або результати, але не повинен дублювати весь production engine тільки тому, що фронт-офіс продає вироблені товари.
Це загальне правило архітектури Base2Base: обчислювати складну бізнесову істину там, де вона належить, і реплікувати результат туди, де він потрібен для операцій. Воно захищає від появи двох незалежних реалізацій FIFO або виробничої собівартості з потенційно різними результатами.
40. WebSite, QR Menu і каса — різні форми одного фронт-офісного принципу
Каса Shopdesk, QR Menu та інтернет-магазин мають різний UI, але з точки зору центральної архітектури схожі:
- їм потрібен каталог або підмножина товарів;
- їм потрібні актуальні ціни й доступність;
- вони працюють у контексті певних підрозділів;
- вони можуть створювати замовлення, продажі або інші документи;
- результати мають потрапити до Shopserver і далі в облікову систему.
Різниця полягає у бізнес-сценарії. На Shopdesk продавець керує чеком. У QR Menu покупець сам формує замовлення. На Хорошоп покупець працює з зовнішньою e-commerce платформою. Але Shopserver дозволяє не створювати для кожного каналу окремий паралельний світ обліку.
---
41. Як читати сторінки кабінету під час підтримки
Кабінет варто сприймати як набір проєкцій одного розподіленого процесу.
«Довідник товарів» відповідає: що за номенклатура відома Shopserver.
«Товарні запаси»: у яких підрозділах ця номенклатура представлена, з якими оперативними параметрами.
«Каси Shopdesk»: які фронт-офісні робочі місця зареєстровані.
«Робочі зміни»: як працювали каси у часі.
«Контейнери документів»: які бізнес-події реально надійшли від клієнтів.
«Завантаження довідників»: коли й як back-office оновлював оперативний стан Shopserver.
«Web-сайти»: як налаштовані e-commerce канали й з яких підрозділів вони беруть дані.
«Стоп-лист», Quick Menu, QR Menu: локальні правила доступності та представлення асортименту.
Це дає природний алгоритм діагностики: спочатку визначити, який саме етап процесу користувач описує, а потім перейти до відповідного розділу кабінету.
---
42. Типові питання і правильний напрямок пошуку
«Чому товар не видно на касі?»
Перевірити, чи є товар у Shopserver, чи є потрібний InventoryRecord для підрозділу каси, чи успішно пройшла реплікація, чи каса завантажила нові дані. Якщо товар був «видалений» у back-office, врахувати семантику реплікації та можливу потребу початкової реплікації.
«Чому на сайті немає товару, хоча він є в довіднику?»
Перевірити, чи товар доданий до каталогу конкретного WebSite, чи прив'язаний до правильної категорії, які підрозділи дозволені сайту, чи має товар там доступний запас/ціну.
«Чому після продажу залишок не змінився?»
Перевірити шлях контейнера: Shopdesk → Shopserver → DownloadContainers обліковою системою → проведення документів → наступна реплікація InventoryRecord → каса/сайт.
«Де подивитись, чи чек дійшов?»
Почати з Containers, потім подивитись LogRecords/історію контейнера, завантаження обліковою системою та approve-result.
«Чим магазин відрізняється від складу в системі?»
На верхньому рівні обидва є підрозділами компанії. Відмінність задається функціями та участю у бізнес-процесах. Магазин зазвичай має каси й продажі, склад — зберігання й переміщення, але модель не обмежується цими назвами.
«Чому продаж із Хорошоп не одразу закривається оплатою?»
Тому що товарний продаж і фактичне надходження грошей від платіжної системи можуть бути різними подіями. Видаткова накладна може потрапити в облік раніше, а платіж від LiqPay/NovaPay — окремо пізніше.
---
43. Коротка технічна карта проєктів
AndriyCo.Site2023
Веб-сайт Base2Base й авторизований кабінет. Для Shopserver містить Pages/Shopserver, Datasources/Shopserver і загальні channel/datasource abstractions. Має залежність від AndriyCo.Shopserver.Proxy, але не від AndriyCo.Shopserver.Data.
AndriyCo.Shopserver.Proxy
Контрактний шар: DTO/proxy для товарів, запасів, кас, контейнерів, WebSites, меню, користувачів, журналів та інших API-об'єктів.
AndriyCo.Shopserver
ASP.NET Core API. Містить контролери, авторизацію, SignalR hubs, background services, MassTransit consumers та інтеграції з зовнішніми сайтами.
AndriyCo.Shopserver.Data
EF Core і доменна логіка. Основні контексти — DictionaryContext і DocumentContext.
AndriyCo.Shopserver.OnlineStore
Окрема runtime-вітрина, яка отримує підготовлений snapshot каталогу від Shopserver, а не читає його внутрішню ORM-модель.
---
44. Основні UI-маршрути кабінету Shopserver
/Shopserver/Settings— налаштування поточної компанії в Shopserver./Shopserver/GoodsItems— загальний довідник товарів./Shopserver/InventoryRests— товарні запаси по підрозділах./Shopserver/Shopdesks— облікові записи кас Shopdesk./Shopserver/DataTerminals— термінали збору даних./Shopserver/ScaleServers— вагові сервери./Shopserver/Cashiers— касири./Shopserver/TransactionTypes— статті руху./Shopserver/WorkShifts— робочі зміни кас./Shopserver/Containers/0— контейнери документів./Shopserver/ExchangeSessions— сеанси завантаження довідників./Shopserver/QuickMenus— меню швидкого доступу./Shopserver/QrMenus— QR Menu./Shopserver/Stoplist— стоп-лист./Shopserver/WebSites— канали e-commerce / веб-сайти./Shopserver/Info/DayBottomLine— оперативні підсумки за добу./Shopserver/Info/DayBottomLineCompare— порівняння добових підсумків./Shopserver/Info/PeriodChart— підсумки за період./Shopserver/Info/ZRevision/...— ревізія Z-звітів.
---
45. Основні серверні області API
Цей список потрібен як технічний індекс, але не повинен бути основним джерелом людської відповіді.
AccountingSystemController— реплікація довідників і обмін контейнерами з back-office.CompanyController— поточна компанія й налаштування.GoodsItemsController— товари.InventoryRecordsController— товарні запаси.DepartmentsController— підрозділи.ContractorsController— контрагенти.CashiersController— касири.TransactionTypesController— статті руху.ShopdeskController— управління касами.BridgeController— runtime-взаємодія каси із сервером.DataTerminalController— термінали збору даних.ScaleServerController/ScaleServerApiController— вагові сервери.ContainersController— кабінетний доступ до контейнерів.LogRecordsController— журнали контейнерів.QuickMenuController,QrMenuController,StopListController— меню та доступність товарів.WebSitesController— адміністративна модель сайтів.OnlineStoreExportController— snapshot для внутрішнього OnlineStore.TccController,Tcu5Controller— точки інтеграції з іншими сервісами.InfoController— оперативна аналітика.
---
45A. Каталог ключових архітектурних рішень
Нижче — стислий індекс рішень, який корисно індексувати окремими RAG-чанками.
| Рішення | Яку проблему вирішує | Основний наслідок | |---|---|---| | Shopserver між TCU та front-office | уникнути прямих зв'язків усіх систем між собою | центральний оперативний integration hub | | Site залежить від Proxy, не від Data | не змішувати UI та persistence model | API/contract boundary | | DictionaryContext + DocumentContext | різні профілі state та event/history даних | різні БД/життєві цикли, eventual consistency між ними | | CurrentCompany після server-side authorize | tenant isolation | Company context не довіряється довільному параметру UI | | GoodsItem окремо від InventoryRecord | один товар у багатьох підрозділах | стабільна номенклатура + високочастотний оперативний стан | | Інкрементальна реплікація + initial repair | великі довідники та автономні клієнти | дешеві дельти, окремий механізм повного відновлення | | Container + DocumentGuid | складні операції та повторна доставка | store-and-forward та idempotency | | TPH для контейнерів | багато типів документів зі спільним lifecycle | єдиний журнал/API та поліморфна поведінка | | SignalR тільки як notification | low latency без втрати надійності | authoritative state залишається durable | | MassTransit для service events | не блокувати HTTP та не зв'язувати сервіси напряму | asynchronous fan-out | | DeleteBehavior.Restrict | захист історичних/зв'язаних даних | видалення стає явним і контрольованим | | UseLazyLoadingProxies(false) | передбачуваність API queries | explicit loading/projection | | ExchangeSession | діагностика distributed replication | checkpoint + audit + observability | | WebSite + provider-specific adapter | різні зовнішні e-commerce API | стабільне ядро та змінні інтеграційні адаптери | | Background services | housekeeping/sync поза request path | незалежний lifecycle фонової роботи |
Як використовувати цей каталог у відповідях асистента
Якщо користувач ставить питання «чому?», retrieval повинен намагатися витягнути не лише chunk із назвою функції, а й відповідний architectural-decision chunk. Наприклад, на питання «чому сайт не читає Shopserver DB напряму?» найціннішим буде не опис GoodsItemsController, а chunk про contract boundary Site → Proxy → API → Data.
---
46. Глосарій предметної області
Компанія — верхній tenant-рівень, власник довідників, пристроїв, документів і налаштувань.
Підрозділ — універсальне місце операцій і/або зберігання цінностей: магазин, склад, офіс, виробничий цех, банківський рахунок тощо.
Товар (GoodsItem) — номенклатурна картка, а не кількість товару на складі.
Товарний запас / InventoryRecord — оперативний стан певного товару в конкретному підрозділі.
Закупівельна партія — у TCU фактично рядок прибуткової накладної з власною закупівельною ціною, початковою й поточною кількістю.
FIFO — відвантаження спочатку з найстаріших доступних партій.
FEFO — відвантаження в першу чергу з партій, у яких раніше спливає термін придатності.
Прибуткова накладна — надходження товару від постачальника або інший прихід товарних цінностей.
Видаткова накладна — відвантаження/продаж товару отримувачу.
Касовий ордер — документ руху коштів; може бути прибутковим або видатковим.
Накладна на передачу — переміщення товару між підрозділами однієї компанії.
Переоблік — звірка фізичного і облікового залишку з проведенням коригуючих змін.
Стаття руху (TransactionType) — класифікація економічного сенсу операції для документів і звітності.
Контрагент — сторона господарської операції: постачальник, покупець, платіжна система або інший партнер.
Shopdesk — фронт-офісне робоче місце касира з локальною реплікою даних.
Shopserver — центральний серверний хаб між back-office, касами, сайтами та іншими фронт-офісними системами.
Контейнер реплікації — пакет довідників та оперативного стану, що передається переважно від облікової системи до Shopserver.
Контейнер документів — транспортна оболонка одного або декількох пов'язаних первинних документів, що переважно рухається від фронт-офісу до Shopserver і далі до облікової системи.
ExchangeSession — журнал сеансу завантаження/оновлення довідників у Shopserver.
WebSite — обліковий запис окремого e-commerce каналу з власним каталогом, підрозділами й правилами синхронізації.
Quick Menu — спеціально організований набір товарів для швидкого вибору на фронт-офісі.
QR Menu — клієнтський веб-канал меню й замовлення, часто для кафе/фастфуду.
TCC — CRM і програма лояльності Base2Base.
TCU — back-office облікова система Base2Base.
---
47. Рекомендована логіка chunking для тришарового документа
Цей документ варто різати не за фіксованими 1000–1500 символів, а за логічними підрозділами. Оптимальна одиниця retrieval — один завершений предметний концепт або один наскрізний сценарій.
Наприклад, розділ «Підрозділ компанії» повинен залишатися одним chunk або кількома сусідніми chunk із повторенням базового визначення. Тоді запит «чи може банківський рахунок бути підрозділом» і запит «чому залишок прив'язаний до DepartmentId» знайдуть той самий понятійний вузол.
Рекомендована metadata:
system: Shopserver
knowledge_type: DomainConcept | BusinessScenario | TechnicalArchitecture | UiGuide | Integration
area: Goods | Inventory | Shopdesk | Replication | Documents | WebSites | Horoshop | Security | Analytics
source_kind: CodeAndSupportArticles
source_version: 2026-08-21
Для технічних chunk можна додавати:
ui_route: /Shopserver/InventoryRests
controller: InventoryRecordsController
project: AndriyCo.Shopserver
client_project: AndriyCo.Site2023
При retrieval для звичайного користувацького питання варто давати вищу вагу DomainConcept і BusinessScenario, а TechnicalArchitecture використовувати для уточнення. Для інтеграційних і діагностичних питань — навпаки, піднімати TechnicalArchitecture та Integration.
---
48. Межі цього опису
Цей документ описує Shopserver і ту частину кабінету Base2Base, яка з ним безпосередньо працює. Для пояснення предметних понять використано статті про TCU, Shopdesk, Хорошоп, виробництво, товарний облік та інші суміжні області.
Це не означає, що повна логіка TCU або TCC реалізована всередині Shopserver. Там, де стаття описує back-office модель, вона використовується як пояснення походження і сенсу даних. Фактичні межі Shopserver визначені поточним source code.
Окремі рішення мають історичні та legacy-шари. Support Center містить як сучасні, так і старі матеріали про Shopdesk/TCU. У цьому документі пріоритет надано концепціям, які узгоджуються з поточною архітектурою та кодом, а не буквальному відтворенню кожної старої інструкції.
Наступні логічні документи, які доповнять цю модель: окрема предметно-технічна модель Trade Control Center, окрема модель Trade Control Utility і після цього — загальна карта екосистеми Base2Base з перетинами сутностей і потоками даних між усіма сервісами.
49. Підсумкова модель мислення про Shopserver
Після об'єднання предметного, архітектурного й технічного шарів Shopserver найкраще описується так.
TCU та інші back-office системи володіють складною первинною бізнес-логікою. Вони знають, як проводити документи, вести партії, FIFO/FEFO, виробництво, взаєморозрахунки та звітність.
Shopserver володіє оперативною інтеграційною моделлю. Він приймає й розповсюджує довідниковий стан, ідентифікує компанії та пристрої, зберігає контейнери подій, веде журнал доставки, дає API каналам продажу й запускає асинхронні реакції.
Front-office володіє локальним процесом взаємодії з користувачем. Shopdesk має бути швидким та автономним; сайт — зручним каналом e-commerce; Data Terminal — інструментом складської операції; Cookdesk — робочим місцем кухаря. Кожен клієнт отримує лише потрібну проєкцію загальної моделі.
Proxy/DTO є мовою між цими світами. Внутрішні EF entities не стають публічним контрактом.
Distributed consistency досягається не глобальною транзакцією, а протоколом. Інкрементальні сеанси, ContainerId, DocumentGuid, журнали, підтвердження, початкова реплікація та повторна доставка разом формують надійність системи.
Real-time технології доповнюють, а не замінюють durable state. SignalR прискорює реакцію, MassTransit розв'язує серверні процеси, але SQL/контейнери залишаються джерелом відновлюваної історії.
Кабінет Base2Base є проєкцією сервісів, а не власником їхніх даних. Його задача — зробити серверну модель доступною людині, не перетворюючи вебсайт на ще один Shopserver.
Саме ця система координат дає найкращий спосіб відповідати на складні питання: спочатку визначити, кому належить бізнесова істина; потім — яким шляхом вона реплікується або транспортується; далі — який сервіс/контракт/контекст реалізує цей шлях; і лише після цього шукати конкретну сторінку або endpoint.
Andriy Kravchenko
Admin, Writer, File Uploader
Останнє оновлення:
8/21/2026 7:06:14 PM
8