Trade Control Utility: предметна, архітектурна та технічна модель системи
Trade Control Utility: предметна, архітектурна та технічна модель системи
Trade Control Utility (TCU) — back-office система Base2Base для ведення внутрішнього товарного і фінансового обліку компанії. Вона використовується в магазинах і торгових мережах, на складах, у кафе, ресторанах, виробничих цехах та інших підприємствах, де потрібно відображати рух товарів, коштів, взаєморозрахунки, виробництво, переоцінки, переобліки та інші господарські операції.
Цей матеріал навмисно не є ані інструкцією користувача, ані переліком класів програми. Його мета — описати Trade Control Utility одразу у трьох площинах:
- предметній — які реальні економічні об'єкти та процеси моделює система;
- архітектурній — які програмні рішення були обрані, яку проблему вони вирішують і чому система побудована саме так;
- технічній — якими проєктами, класами, сутностями, API та фоновими процесами ці рішення реалізовані.
Таке поєднання важливе для правильного розуміння TCU. Наприклад, InventoryRecord без предметного контексту виглядає просто як таблиця залишків. Але в архітектурі TCU це матеріалізований поточний стан, який підтримується на підставі історії господарських операцій. А InventoryJournalRecord — не просто журнал аудиту, а один із фундаментальних носіїв історії руху товару та його зв'язку із закупівельною партією.
Детальніше: Загальна інформація про Trade Control Utility; Архітектура сервісів Base2Base.
1. Як читати цей матеріал: факт, архітектурний висновок та цільова модель
Під час аналізу великої системи, яка розвивається багато років, важливо не змішувати три різні типи тверджень.
Підтверджений факт реалізації — те, що безпосередньо видно в коді або документації. Наприклад, WinForms-клієнт використовує AndriyCo.Tcu5.DataSources та AndriyCo.Tcu5.Proxy; серверний застосунок працює з AndriyCo.Tcu5.Data; Document.Approve() виконує перевірки, змінює статус документа і викликає спеціалізований InnerApprove().
Архітектурний висновок — пояснення, яке випливає зі структури системи. Наприклад, виділення Proxy в окремий проєкт з netstandard2.0 дозволяє клієнту й серверу мати спільний контракт без перенесення серверної EF-моделі у WinForms-клієнт.
Цільова модель — актуальний напрямок розвитку, який не завжди повністю збігається з історичним кодом. Найважливіший приклад — пряма інтеграція TCU з Prom.ua. У кодовій базі вона існує, але це історичне рішення. У цільовій архітектурі інтеграції з e-commerce платформами мають концентруватися в Shopserver, як уже зроблено для Хорошопу. TCU повинна залишатися власником облікової предметної моделі, але не знати специфічні API кожного зовнішнього торгового майданчика.
Це розрізнення особливо важливе для AI-асистента: наявність класу в solution ще не означає, що саме так повинна виглядати майбутня архітектура.
2. Місце TCU в екосистемі Base2Base
Trade Control Utility є back-office системою. Її основна задача — відображати внутрішні господарські процеси підприємства.
З погляду ролей екосистеми:
- TCU веде товарний і фінансовий облік, документи, залишки, партії, взаєморозрахунки, виробництво, звітність;
- Shopdesk є front-office робочим місцем касира;
- Shopserver є центральним сервісом взаємодії front-office, back-office та e-commerce каналів;
- Trade Control Center відповідає за CRM, покупців, бонуси, маркетингові механізми та інші функції лояльності;
- допоміжні системи на кшталт Scale Server, Data Terminal, Cookdesk або OnlineStore вирішують свої спеціалізовані задачі.
TCU не повинна перетворюватися на універсальний інтеграційний моноліт, який безпосередньо знає протоколи всіх зовнішніх сервісів. Її сильна сторона — предметний облік.
Детальніше: Архітектура сервісів Base2Base; Shopserver. Схема обміну даними.
3. Фізична архітектура: rich client, application server, database і background worker
Поточна TCU має три основні виконувані рішення:
Tcu5Client.sln— WinForms rich client;Tcu5Server.sln— сервер бізнес-логіки, тобто application server;Tcu5CycleServer.sln— окремий host для циклічної та фонової обробки.
У спрощеному вигляді:
WinForms client
↓
DataSources + Proxy contracts
↓ HTTP / SignalR
Tcu5Server
↓
AndriyCo.Tcu5.Data
↓
SQL Server database of company
паралельно:
Tcu5CycleServer
↓
AndriyCo.Tcu5.Data
↓
ті самі company databases
Це не просто фізичне рознесення програм. Воно формує межі відповідальності.
WinForms-клієнт відповідає за роботу людини: форми, редактори, списки, діалоги, навігацію, інтерактивну звітність.
Application server відповідає за авторизацію, доступ до конкретної бази компанії, виконання бізнес-операцій, проведення документів, звітні запити, інтеграції та серверну координацію.
CycleServer виконує роботу, яка не повинна залежати від відкритого клієнта або HTTP-запиту: планові задачі, синхронізації, ABC-аналіз, очищення, роботу з файлами та інші довготривалі процеси.
4. Чому TCU є rich-client системою, а не «тонким веб-клієнтом»
TCU розвивалася як робочий інструмент співробітника, який багато годин працює з великими таблицями, документами, довідниками, звітами й редакторами. WinForms-клієнт у такій системі є не випадковою спадщиною, а логічним результатом вимог до інтенсивної desktop-роботи.
Клієнт містить сотні форм та редакторів. Але важливо, що це не означає перенесення всієї бізнес-логіки на клієнт. Архітектурний напрямок сучасної TCU — зберігати на клієнті логіку взаємодії з користувачем, а критичну предметну логіку виконувати на сервері.
Тому проведення документа, перевірка прав, робота із закупівельними партіями або зміна поточного складського стану не повинні залежати від того, який саме WinForms-редактор ініціював операцію.
5. AndriyCo.Tcu5.Proxy: контракт між клієнтом і сервером
AndriyCo.Tcu5.Proxy є одним із ключових проєктів архітектури.
Він містить proxy/DTO-моделі для довідників, товарів, підрозділів, контрагентів, документів, документних рядків, залишків, журналів руху, фінансових записів, пакетів документів, звітів, планових задач, матриці та інтеграційних даних.
Проєкт орієнтований на netstandard2.0, що дозволяє використовувати ті самі контракти і .NET Framework WinForms-клієнту, і сучасному .NET серверу.
Чому не передавати EF entities напряму
Внутрішні entities AndriyCo.Tcu5.Data мають поведінку, доступ до TcuContext, навігаційні властивості, persistence-деталі та службові правила.
Proxy-рівень відокремлює внутрішню серверну модель від даних, які дозволено бачити і змінювати клієнту. Це зменшує зв'язність і дає змогу еволюціонувати data model незалежніше від UI.
6. AndriyCo.Tcu5.DataSources: клієнтський application layer
Між WinForms UI та HTTP API є ще один важливий шар — AndriyCo.Tcu5.DataSources.
Його структура повторює функціональні області TCU: Goods, Departments, Inventory, Documents, Contractors, Reports, Schedule, Matrix, ExternalSystems, Search.
Базовий DataSource створює TcuChannel, знає route відповідного серверного контролера, працює з bearer token і надає UI більш предметний інтерфейс.
Тому форма в ідеальному випадку не повинна знати повний URL endpoint, спосіб серіалізації, bearer header, деталі SignalR або transport-модель. Вона працює з GoodsListDataSource, DocumentEditorDataSource, InventoryRecordListDataSource тощо.
7. TcuChannel: одна транспортна точка доступу до Tcu5Server
TcuChannel успадковує bearer-aware channel та використовує адресу сервера, порт, API prefix і bearer token із ITcuDataSourceContext.
Архітектурне значення цього класу більше, ніж його розмір: він централізує сам факт того, що дані TCU отримуються через application server. Клієнтський код не повинен конструювати доступ до SQL Server і не повинен сам визначати базу конкретної компанії.
8. Multi-tenant модель: окрема база даних компанії
TCU працює в multi-tenant середовищі, але важливий спосіб реалізації: для компанії використовується своя TCU database.
Application server спочатку визначає контекст компанії через Arbiter. TcuControllerBase.CreateTcuContext() створює UserTcuContext, після чого контролер працює вже з конкретною базою.
У контролері зберігаються діагностичні атрибути CompanyName, CompanyId, CompanyGuid, DatabaseName.
Така модель створює сильну фізичну межу між обліковими даними різних компаній.
Чому не достатньо передати CompanyId з клієнта
Клієнт не повинен бути джерелом істини щодо tenant-а. Bearer identity і серверний Arbiter визначають, до якої company database має бути створений контекст.
9. UserTcuContext і SystemTcuContext: людина та внутрішня система працюють у різних контекстах
У data layer існують спеціалізовані контексти:
UserTcuContext— операція виконується від імені звичайного користувача;SystemTcuContext— операція виконується внутрішньою системою або background service.
TCU має багато операцій, які виконуються без людини: scheduled task, імпорт документів із Shopserver, синхронізація зовнішньої системи, автоматична переоцінка, виправлення партій, ABC calculation.
Такі операції не варто маскувати під випадкового desktop-користувача.
10. Предметна модель починається не з таблиць, а з господарських операцій
У центрі TCU знаходиться ідея, що стан підприємства змінюється через операції.
Товар не «просто стає більшим або меншим». Відбувається подія: надходження, продаж, передача, списання, переоблік, переоцінка, виробництво. Кошти так само надходять, витрачаються або передаються.
Ці події формалізуються документами.
Саме тому TCU краще розуміти не як набір довідників і таблиць, а як систему відтворення господарської історії компанії через документи та похідний від них стан.
Детальніше: Розділ «Документи»; Товарні документи; Облік фінансів у Trade Control Utility.
11. Універсальна абстракція Document
Document — центральна бізнесова абстракція TCU.
На рівні EF Core використовується discriminator DocumentType, а конкретні типи документів є спадкоємцями: PurchaseInvoice, SalesInvoice, CustomerReturnOrder, SupplierReturnOrder, GoodsTransferNote, Revaluation, Correction, Production, WriteOff, PayInSlip, PayOutOrder, TransferOrder, CustomerOrder та інші.
Це дозволяє мати спільний життєвий цикл для різних господарських операцій.
Документ має загальні атрибути: номер, Guid, дату створення, дату облікового проведення, реальний час проведення, статус, підрозділ, користувача, валюту і курс, статтю руху, endpoint, суму, джерело, зв'язок із верхнім документом, інформацію про front-office та ShopserverContainerId.
12. Чому спадкування документів тут виправдане
Прибуткова накладна і виробничий акт відрізняються поведінкою, але обидва мають статус, можуть бути проведені й відкочені, перевіряють права, належать підрозділу, створюють наслідки в обліку та можуть мати пов'язані документи.
Тому спільний базовий Document концентрує життєвий цикл, а спеціалізовані класи реалізують відмінності через InnerApprove, InnerRollback і sequence checks.
13. Проведення документа — не зміна прапорця Status
Ключовий метод — Document.Approve().
Він послідовно записує початок операції, завантажує необхідні навігаційні дані, виконує IsCanApprove(), встановлює реальний час проведення, фіксує currency revaluation, змінює статус, встановлює користувача, викликає спеціалізований InnerApprove(), зберігає зміни та журналює успішне завершення.
IsCanApprove() перевіряє, серед іншого:
- чи документ уже не проведений;
- чи підрозділ не закритий;
- чи не дублюється документ за Guid;
- чи має користувач право;
- чи не потрапляє документ у закритий період;
- чи не порушує він послідовність залежних операцій.
Отже «провести документ» означає застосувати господарську операцію до стану системи з повним набором перевірок.
14. Відкат — симетрична бізнес-операція
Document.RollBack() також є повноцінною операцією: перевіряє можливість відкату, змінює статус, викликає InnerRollback(), відновлює похідний стан, зберігає зміни й журналює результат.
TCU не дозволяє довільно «стерти наслідки» документа. Для відкату діють права, закритий період і перевірки послідовності.
Детальніше: Закриття звітного періоду в Trade Control Utility.
15. Закритий період належить підрозділу, а не всій компанії
Department містить LastDateInClosedPeriod та пов'язані атрибути.
Це відповідає реальному сценарію: один магазин може вже завершити переоблік і закрити період, а інший підрозділ тієї самої компанії ще працює з попередньою датою.
Тому перевірка закритого періоду входить у життєвий цикл Approve і Rollback.
16. Department: універсальна точка обліку
У TCU підрозділ — значно ширше поняття, ніж «магазин».
Department успадковує Point і може мати типи stock, workshop, online store, cashbox та інші комбінації. Підрозділ має ієрархію через TopDepartmentId.
До нього прив'язуються документи, InventoryRecord, MoneyRecord, права користувачів, нумератори документів, податкові параметри, банківські й фіскальні властивості, закритий період та інтеграційні link-и.
З предметного погляду підрозділ є місцем, де відбуваються операції або зберігаються цінності.
Детальніше: Довідник підрозділів; Картка підрозділу компанії.
17. Чому немає окремих фундаментальних моделей «магазин», «склад», «цех»
Універсальний Department дозволяє не дублювати базову облікову логіку.
Незалежно від назви місця, товар усе одно надходить, витрачається, передається, має залишок, може бути переоцінений і може брати участь у переобліку.
Спеціальні можливості виражаються типами та налаштуваннями, а не окремими паралельними обліковими моделями.
18. Товарна картка і товарний залишок — принципово різні речі
GoodsItem описує номенклатурну одиницю: що це за товар.
InventoryRecord описує стан цього товару у конкретному підрозділі.
Один GoodsItem може мати багато InventoryRecord для різних магазинів, складів або цехів.
Це дозволяє мати одну товарну картку, але різні кількості, ціни, середні закупівельні ціни, мінімальні запаси, статуси продажу, статуси приходу, виробничі дозволи та ABC-категорії.
Детальніше: Довідник товарів; Картка товару; Картка обліку товару на складі.
19. InventoryRecord — матеріалізований поточний стан
InventoryRecord має GoodsItemId, DepartmentId, Quantity, Price, MiddlePurchasePrice, LastPurchasePrice, ReservedQuantity, MinQuantity, SpareDays, статуси приходу, продажу й виробництва, параметри націнки та ABC-категорії.
Найважливіше — Quantity змінюється під час проведення або відкату відповідних документних рядків.
При приході Quantity збільшується. При видатку — зменшується. Паралельно перераховується середня закупівельна ціна та інші похідні параметри.
Архітектурно InventoryRecord — це швидко доступна проєкція поточного стану, а не повна історія.
20. Чому поточний залишок зберігається, якщо його можна розрахувати з журналу
Теоретично поточний залишок можна щоразу обчислювати як суму всіх рухів.
Практично для великої торгової системи це занадто дорого для кожного відкриття таблиці, звіту, перевірки наявності чи проведення документа.
Тому TCU підтримує поточну Quantity у InventoryRecord.
Це компроміс:
історія руху → джерело для перевірки та розрахунків
поточний InventoryRecord → швидкий operational state
Детальніше: Облік товарів у Trade Control Utility; Розділ «Товарні запаси».
21. InventoryJournalRecord: історія руху як окрема сутність
Під час проведення товарного документа створюються InventoryJournalRecord.
Запис журналу містить документ, рядок документа, складський запис, дату, напрямок, кількість, закупівельну ціну, ціну продажу, користувача, підрозділ, endpoint, валютні параметри та посилання на верхній документ і партію.
Це не просто technical audit log. InventoryJournalRecord формалізує факт зміни товарного стану.
22. Закупівельна партія в TCU
Коли товар надходить, рядок прибуткового документа формує партію з власною закупівельною ціною та поточною невитраченою кількістю.
Під час видатку DocumentDetail.ApproveIssue() отримує список закупівельних партій через GetPurchasePartiesForIssue(), після чого IssueFromPurchaseParties() послідовно витрачає потрібну кількість із партій.
Для кожної використаної партії створюється journal record, де TopDocumentDetail посилається на відповідний прихід.
Отже система знає не лише «продано 5 кг», а й з яких конкретних приходів ці 5 кг були сформовані.
23. FIFO і середньозважена ціна не суперечать одне одному
TCU одночасно підтримує два рівні інформації.
Для походження конкретного видатку і точної собівартості операції використовується партійний механізм.
Для оперативної оцінки поточного запасу InventoryRecord підтримує MiddlePurchasePrice.
Тому твердження «TCU використовує FIFO» та «TCU має середньозважену закупівельну ціну» описують різні аспекти однієї моделі.
Детальніше: Облік товарів у Trade Control Utility: FIFO, FEFO та середньозважена ціна.
24. Чому партійний облік важливий
Партії дозволяють точно визначити собівартість продажу, простежити походження товару, працювати з різними закупівельними цінами, реалізовувати FIFO і FEFO, аналізувати проблемні партії та відтворювати зв'язок виробу з партіями інгредієнтів.
Без партій поточний залишок говорить лише «скільки», але не відповідає «звідки це взялося і скільки коштувало».
25. Від'ємний залишок і від'ємна партія — не одне й те саме
Загальний InventoryRecord.Quantity може бути коректним, але розподіл по партіях може потребувати корекції після імпорту старих даних, порушення послідовності або інших історичних ситуацій.
Саме тому в TCU існують спеціальні механізми виправлення партій.
Детальніше: Виправлення від'ємних залишків у партіях.
26. Прибуткова накладна: створення нового товарного стану
PurchaseInvoice — класичний приклад приходу. На предметному рівні вона означає отримання товару від постачальника. На рівні обліку проведення створює або знаходить InventoryRecord, збільшує залишок, формує закупівельну партію, фіксує закупівельну ціну, створює записи руху та впливає на взаєморозрахунки.
Детальніше: Прибуткова накладна у Trade Control Utility.
27. Видаткова накладна: продаж як витрата товару
SalesInvoice моделює передачу товару покупцю.
Проведення видаткового рядка визначає SourceInventoryRecord, перевіряє дозволи, визначає закупівельні партії, розподіляє quantity між ними, створює InventoryJournalRecord, обчислює фактичну закупівельну ціну рядка і зменшує поточний складський залишок.
Детальніше: Видаткова накладна; Редактор видаткової накладної.
28. Продаж і оплата — дві різні операції
Передача товару покупцю і отримання грошей — різні господарські факти.
Тому продаж зазвичай розкладається на SalesInvoice + PayInSlip, а закупівля і оплата постачальнику — на PurchaseInvoice + PayOutOrder.
Це дозволяє підтримувати борги, часткові оплати, кілька платежів на один документ, один платіж на кілька документів, різні способи оплати та відстрочку платежу.
Детальніше: Касовий чек, накладна та прив'язка оплат; Касові ордери.
29. MoneyRecord: фінансовий аналог товарного стану
TCU веде не лише товари, а й кошти. Department має колекцію MoneyRecords, а фінансові документи змінюють грошовий стан підрозділу у конкретній валюті.
Предметна симетрія:
GoodsItem + Department → InventoryRecord
Currency + Department → MoneyRecord
Товарний і фінансовий контури різні, але побудовані навколо спільної ідеї: документована операція змінює стан цінностей у певній точці обліку.
Детальніше: Облік фінансів у Trade Control Utility; Прибутковий касовий ордер; Видатковий касовий ордер.
30. Стаття руху як економічний зміст операції
TransactionType присутній у базовому Document. Документ знає не лише, що технічно відбувся прихід або видаток, а й економічний зміст руху. Це важливо для фінансової звітності.
Детальніше: Статті руху товарів та коштів.
31. Передача між підрозділами — одна бізнес-подія з двома сторонами
Накладна на передачу відображає переміщення товару всередині однієї компанії. Для підрозділу-відправника це видаток. Для підрозділу-отримувача — прихід.
У моделі існують GoodsTransferNote і GoodsTransferNoteIncome, а документ знає Department2. Це дозволяє зберігати правильну семантику обох сторін, не перетворюючи внутрішнє переміщення на фіктивний продаж.
Детальніше: Накладна на передачу товару.
32. Пов'язані та дочірні документи
Document має TopDocumentId, TopDocumentGuid, SourceDocumentId і SourceDocumentGuid.
У коді прямо зафіксоване поняття первинного документа: IsPrimaryDocument істинний, якщо немає TopDocumentId.
Пов'язаний документ може бути створений у процесі проведення іншого документа і повинен жити разом із ним. Це використовується в автоматичних бізнес-сценаріях, де одна дія користувача породжує кілька облікових наслідків.
33. Пакети документів: одна бізнес-команда може породити багато документів
У TCU існує окрема модель Package і численні спеціалізації: packages by departments, revaluation packages, negative rests correction packages, transfer packages, supplier returns, online-store packages, equipment revision packages.
Коли користувач хоче «переоцінити цю групу товарів у 40 магазинах», системі потрібен об'єкт, який координує створення та проведення багатьох документів.
Пакет є orchestration-рівнем над документами.
34. Переоцінка: ціна теж має історію
Ціна продажу не повинна просто непомітно змінитися в InventoryRecord.
Для значущої зміни ціни існує документ Revaluation. Це дозволяє знати, коли змінилася ціна, яка була попередня, яка стала новою, хто виконав операцію і до якого товару та підрозділу вона належала.
Переоцінка також вплітається в sequence checks: пізніша переоцінка може обмежувати можливість відкату попереднього документа.
Детальніше: Робота з цінами у Trade Control Utility; Переоцінка за розкладом.
35. Переоблік: фактичний світ коригує обліковий
Облікова система може точно виконати всі введені операції, але реальний склад усе одно може відрізнятися через крадіжки, псування, помилки приймання чи пересорт.
Переоблік — це введення фактичного стану та створення документованої корекції між обліковим і реальним залишком.
Це фундаментальна властивість товарного обліку: інформаційна модель повинна мати механізм повернення до фізичної реальності.
36. Виробництво: товар може бути результатом перетворення інших товарів
Production розширює товарну модель.
TCU використовує Recipe, RecipeDetail, Production, ProductionDetail, ProducedItemDetail, IngredientDetail.
Проведення виробництва витрачає інгредієнти, прив'язує їх до конкретних партій, визначає фактичну собівартість і створює прихід готового виробу.
Детальніше: Технологічна карта страви та облік виробництва.
37. Рекурсивне виробництво і напівфабрикати
Виріб може використовувати інший виріб як компонент. Тому виробнича модель фактично рекурсивна.
У системі немає фундаментальної різниці між «готовим товаром» і «напівфабрикатом» — контекст використання визначає роль. Один товар може продаватися окремо і водночас входити до рецептури іншого виробу.
38. Тягнуча схема виробництва
TCU підтримує сценарій, коли під час видатку готового виробу його недостатня кількість автоматично довиробляється.
Видаткова накладна може спричинити створення пов'язаного Production. Виробництво витрачає партії інгредієнтів, за потреби рекурсивно працює з напівфабрикатами, готовий виріб надходить, після чого продаж завершує видаток.
Саме для таких сценаріїв документам потрібні top/source links і dependency-aware rollback.
39. Ціноутворення живе на стику глобального товару та локального складського запису
У GoodsItem є загальні правила та параметри, а InventoryRecord має локальні Price, MarkupPercent, RoundTo, UseMarkup, IsInheritedMarkup.
Це дозволяє мати спільну номенклатуру, але різні ціни по магазинах або інших підрозділах.
Детальніше: Робота з цінами у Trade Control Utility.
40. Товарна матриця як керування локальною поведінкою товарів
Коли мережа велика, редагувати кожен InventoryRecord окремо незручно.
Товарна матриця дозволяє керувати параметрами товарів по групах підрозділів: асортиментом, дозволом продажу, приходом, виробництвом, цінами, націнками та іншими локальними правилами.
Детальніше: Товарна матриця у Trade Control Utility.
41. Оптимістична конкурентність
Ключові entities мають RowVersion.
Document.Approve() окремо обробляє DbUpdateConcurrencyException і розрізняє конфлікти для InventoryRecord, Document, DocumentDetail, Department, Contractor, GoodsItem.
Для багатокористувацької системи це критично: одночасна робота не повинна перетворюватися на «останній записав — той правий».
42. Права доступу є частиною предметної операції
Document.IsCanApprove() перевіряє право на конкретну операцію в конкретному підрозділі.
Модель прав відповідає на питання: «чи може цей користувач провести саме цей тип документа саме в цьому підрозділі?»
43. Application server як власник критичних правил
Tcu5Server — не просто REST facade над таблицями.
Саме на сервері виконуються авторизація, вибір company database, транзакційні операції, проведення, видалення, звітність, синхронізація, публікація змін і серверні інтеграції.
Клієнт може бути багатим UI, але не повинен бути довіреним місцем для критичної облікової логіки.
44. Чому сервер створює TcuContext на операцію
TcuControllerBase.CreateTcuContext() створює контекст для конкретного authenticated request.
Це дозволяє зв'язати користувача, компанію, базу, права, логування і транзакцію з однією бізнес-операцією.
Контекст не є глобальним singleton із випадково змінним tenant state, що особливо важливо при паралельній роботі багатьох компаній.
45. Транзакції: локальна бізнес-операція має бути атомарною
У server operation controllers використовуються EF Core transactions.
Наприклад, delete flow відкриває transaction, виконує domain delete, зберігає зміни, commit-ить і лише після цього публікує history changes.
У межах однієї TCU database бізнес-операція намагається зберігати атомарність.
46. SignalR: повідомити клієнтів про зміну, а не замінити базу даних
У DataSources є SignalR client dependency, а Tcu5Server має Tcu5SignalHub.
Роль realtime channel — повідомити активні клієнти, що певні дані змінились або що їм потрібно оновити стан.
SignalR не є джерелом даних. Надійна модель: спочатку операція змінює database, потім server повідомляє клієнти, після чого клієнт перечитує актуальні дані.
47. MassTransit: події між сервісами, а не інтерактивний CRUD
У Tcu5Server і Tcu5CycleServer присутній MassTransit.
Він використовується там, де зміна в TCU може бути цікавою іншому сервісу, але не повинна вимагати синхронної відповіді у межах поточного HTTP request.
HTTP, SignalR і MassTransit вирішують різні задачі.
48. Tcu5CycleServer: сервер фонової та планової обробки
Tcu5CycleServer слід розглядати як окремий background processing host.
У ньому існує TcuServicePool, який знаходить актуальні company connections та запускає фонові сервіси для потрібних баз.
Серед видимих сервісів: ScheduleService, UsersUpdateService, CleaningService, ExternalSystemService, AbcCalculationService, FileService.
Чому це винесено окремо
Фонові задачі працюють постійно, запускаються за розкладом, повинні переживати відсутність клієнтів, можуть обробляти багато company databases і не повинні займати HTTP request.
49. TcuServicePool: одна серверна інсталяція обслуговує багато company databases
Service pool отримує актуальні TcuConnectionRow з Arbiter та створює services для конкретних баз.
Background layer теж є tenant-aware. Наприклад, ABC calculation для компанії A і scheduled task для компанії B працюють з різними SystemTcuContext.
50. Призначені завдання: автоматизація як частина предметної моделі
У TCU є ScheduleTask з discriminator за типом задачі.
Плановими можуть бути переоцінки, виправлення залишків, пакетні документи та інші регулярні дії.
Scheduled task є предметним об'єктом TCU і виконує бізнесову операцію через ті самі правила системи.
Детальніше: Розділ «Призначені завдання»; Переоцінка за розкладом.
51. ABC-аналіз як приклад фонової аналітики
AbcCalculationService виконується в CycleServer.
ABC-категорії зберігаються безпосередньо в InventoryRecord: SaleAbcCategory, ProfitAbcCategory, WhenAbcCategoryUpdated.
Це ще один приклад матеріалізованого стану, який зберігається біля operational entity для швидкого використання.
Детальніше: ABC-аналіз продажів у роздрібній торгівлі.
52. Звітність: ще один погляд на предметну модель
У Tcu5Server існують окремі controllers для inventory reports, sales reports, document lists, settlements, P&L, special reports, table reports і report templates.
На клієнті їм відповідають report data sources.
Не кожна цифра обов'язково зберігається як «готовий звітний стан». Частина показників обчислюється з документів, журналів і поточних проєкцій.
Детальніше: Майстер звітів; P&L звіт; Звіт з продажів.
53. Shopserver як інтеграційна межа front-office
TCU має серверну інтеграцію із Shopserver.
Document містить FrontOfficeApp, DateOfSale, CashierId, CashierName, ShopserverContainerId, DocumentSource.
Document.ParseShopserverContainer() читає container від Shopserver, розбирає XML і створює відповідні TCU documents.
Front-office чек не записується напряму у таблиці залишків TCU. Він перетворюється на звичайні предметні документи TCU, після чого проходить стандартне проведення.
Це важливе архітектурне рішення: зовнішнє джерело не отримує окремий «спрощений шлях» повз облікову модель.
54. Ідемпотентність імпорту front-office документів
Document має DocumentGuid, а код містить перевірки існування документа з таким Guid.
Shopserver може повторно доставити container після timeout або повторної спроби. Якщо TCU щоразу створюватиме новий продаж, один чек подвоїть облік.
Стабільний Guid дозволяє розпізнавати вже оброблену бізнес-подію. Це типовий механізм ідемпотентності в store-and-forward інтеграції.
55. Чому TCU не повинна напряму працювати з касами
Якщо кожна Shopdesk напряму звертатиметься до application server TCU, каса стане залежною від back-office availability, TCU доведеться знати життєвий цикл кожної каси, offline-сценарій стане складнішим, а фронт-офісні протоколи проникатимуть у ядро TCU.
Shopserver розриває цю зв'язність.
TCU працює з нормалізованим обмінним контуром, а Shopserver — з фронт-офісними клієнтами.
56. Trade Control Center як інший bounded context
У AndriyCo.Tcu5.Data присутні link-и та proxy dependencies до TCC.
Але CRM-дані не повинні перетворюватися на внутрішню частину товарного журналу TCU.
TCC відповідає за клієнтів, бонусні рахунки, програму лояльності та маркетингові акції. TCU може мати потрібні інтеграційні зв'язки, але source of truth для CRM має залишатися у TCC.
57. E-commerce: історичний код і цільова архітектура
У solution TCU присутні AndriyCo.PromUa, PromUaController, ExternalSystems/Shops/OnlineStores/PromUa і Prom.ua link entities.
Це реальний факт історичного коду, але його не слід трактувати як цільову архітектуру.
Пряма інтеграція TCU ↔ Prom.ua була створена раніше, коли роль Shopserver як універсального інтеграційного шару для зовнішніх каналів ще не була остаточно визначена.
Цільовий напрямок:
TCU
↕
Shopserver
↕
Prom.ua
Horoshop
OnlineStore
інші e-commerce платформи
TCU повинна знати номенклатуру, ціни, залишки, підрозділи й облікові документи. Вона не повинна знати специфічні API кожної торгової платформи.
Детальніше: Архітектура сервісів Base2Base; Shopserver. Схема обміну даними.
58. Історичний код як частина еволюції системи
Великі системи еволюціонують.
Спочатку з'являється конкретна потреба, потім друга подібна інтеграція, після чого стає видно спільний pattern і формується окремий integration service.
Тому архітектуру TCU корисно читати як історію поступового виділення bounded contexts.
59. Data model і domain model у TCU частково збігаються
AndriyCo.Tcu5.Data містить не лише пасивні EF entities.
Багато сутностей мають реальну предметну поведінку: Document.Approve, Document.RollBack, InventoryRecord.ReceiptFrom, InventoryRecord.IssueFrom, DocumentDetail.ApproveIssue, перевірки доступу, пошук партій і розрахунки.
З погляду академічної Clean Architecture це можна було б рознести на більше проєктів. Але фактична важлива властивість системи інша: бізнес-правила виконуються на сервері поруч із моделлю, а не розмазані лише по WinForms forms.
60. Чому не варто механічно «очищати» таку систему заради модної архітектури
TCU — зріла business system з великою кількістю перевірених сценаріїв.
Рефакторинг має сенс там, де він зменшує небезпечну зв'язність, дозволяє незалежно розвивати компоненти, спрощує тестування критичної логіки, прибирає історичну залежність або формує правильний bounded context.
Приклад виправданого руху — винесення Prom.ua до Shopserver.
Приклад сумнівного — переписати робочий Document.Approve() у багато додаткових абстракцій лише заради формальної відповідності архітектурному шаблону.
61. Наскрізний сценарій: прихід товару
Користувач створює прибуткову накладну у WinForms-клієнті.
Editor/DataSource формує proxy і звертається до Tcu5Server.
Application server визначає користувача, company database, завантажує документ, перевіряє право і виконує Approve().
Для кожного рядка створюється або знаходиться InventoryRecord, формується закупівельна партія, створюється InventoryJournalRecord, Quantity збільшується і перераховується середня закупівельна ціна.
Результат: історичний документ, історичний рух, невитрачена закупівельна партія і новий поточний залишок.
62. Наскрізний сценарій: продаж товару
Для SalesInvoice визначається SourceInventoryRecord, отримуються purchase parties, кількість списується по партіях, для кожної частини створюється journal record, обчислюється фактична purchase price і поточний залишок зменшується.
Якщо покупець платить, окремий PayInSlip відображає рух коштів та погашення заборгованості.
Таким чином одна побутова фраза «ми продали товар» у моделі розкладається на кілька точних господарських фактів.
63. Наскрізний сценарій: чек Shopdesk
Shopdesk формує front-office container.
Shopserver приймає його, зберігає і робить доступним для back-office.
TCU отримує container, ParseShopserverContainer() перетворює його на TCU documents, перевіряє Guid і проводить документи стандартними domain methods.
Зовнішнє походження документа не скасовує внутрішні правила обліку.
64. Наскрізний сценарій: передача між магазинами
Користувач створює GoodsTransferNote для source і destination departments.
Проведення витрачає товар у джерелі, створює або оновлює стан у приймачі, формує пов'язану приходну сторону і зберігає зв'язок між двома сторонами операції.
Для бізнесу це одна передача. Для обліку — синхронізована зміна двох локальних станів.
65. Наскрізний сценарій: автоматичне виробництво під продаж
Продаж потребує виробу, якого немає у достатній кількості.
Система бачить нестачу, знаходить recipe, створює dependent Production, списує партії інгредієнтів, за потреби працює з напівфабрикатами, оприбутковує готовий виріб і завершує видаток продажу.
Технічно це демонструє, навіщо документам потрібні top/source links і чому rollback має бути dependency-aware.
66. Наскрізний сценарій: планова переоцінка
Користувач один раз створює scheduled task.
CycleServer знаходить company database, визначає, що час задачі настав, запускає її через SystemTcuContext, створює потрібний package/documents і проводить їх стандартною серверною логікою.
Користувацький комп'ютер може бути вимкнений.
Це і є правильна межа між desktop UI та background processing.
67. Source of truth: для чого TCU є головною системою
TCU є source of truth насамперед для внутрішнього облікового контуру:
- господарських документів;
- товарних рухів;
- закупівельних партій;
- поточного облікового залишку;
- собівартості;
- внутрішніх підрозділів;
- фінансових рухів і взаєморозрахунків;
- виробничих документів.
Але TCU не повинна автоматично вважатися source of truth для всього, що вона може показати або кешувати.
CRM належить TCC. Front-office delivery state належить Shopserver. Специфічна e-commerce модель платформи має належати інтеграційному шару Shopserver.
68. Поточний стан і історія: головна філософія TCU
Одна з найважливіших ідей TCU:
господарська історія зберігається як рух, а поточний стан підтримується як похідна operational projection.
У товарному контурі:
Document
↓
DocumentDetail
↓
InventoryJournalRecord
↓
InventoryRecord current state
Це дозволяє одночасно швидко показувати поточний залишок, знати, як він утворився, простежувати партії, розраховувати собівартість, виконувати ревізії та аналізувати історію.
Саме тому TCU не є системою «збережених знімків залишків на кожен день».
69. Чому цей підхід добре масштабується предметно
Новий тип документа не обов'язково потребує нової паралельної системи стану.
Якщо він змінює товар — породжує товарний рух. Якщо змінює кошти — породжує фінансовий рух. Якщо координує кілька документів — може бути package/orchestrator.
Тому універсальна модель документів і рухів дозволяє розширювати функціонал без появи окремого «міні-обліку» для кожного нового сценарію.
70. Денормалізація як свідомий компроміс
У системі є збережені похідні значення: InventoryRecord.Quantity, MiddlePurchasePrice, ABC categories, EndPointId для швидких списків документів, full paths та інші operational properties.
Нормалізація бази не є самоціллю.
Якщо значення дуже часто читається, дорого обчислюється, має зрозумілий механізм оновлення і може бути перевірене з історії, його матеріалізація виправдана.
71. Auditability: система повинна пояснювати результат
Для облікової системи недостатньо сказати «залишок 17».
Потрібно мати можливість відповісти, якими документами він сформований, з яких партій, хто провів документи, коли, які ціни використовувалися і які залежності існували.
InventoryJournalRecord, documents, approve records, related documents та links забезпечують цю пояснюваність.
72. Що означає «сервер бізнес-логіки» у TCU
Tcu5Server є довіреною точкою, де identity перетворюється на UserTcuContext, tenant — на company database, команда — на domain operation, domain operation — на транзакційні зміни стану, а зміна може породити realtime notification або integration event.
Тому «сервер бізнес-логіки» дуже точно описує його роль.
73. Що означає Tcu5CycleServer у сучасних термінах
Найближчі сучасні назви: background worker host, scheduled processing service, job processing service.
Він не є другим application server для інтерактивного UI. Його задача — виконувати бізнес-процеси з іншою часовою моделлю: не «користувач натиснув кнопку зараз», а «умова або розклад вимагає виконати роботу».
74. Межі відповідальності трьох виконуваних застосунків
Tcu5Client
UX, редактори, таблиці, локальна валідація зручності, виклик data sources, відображення прогресу.
Tcu5Server
Authenticated API, tenant resolution, domain operations, transactions, reports, integration endpoints, realtime notifications, service events.
Tcu5CycleServer
Jobs, schedules, periodic integrations, housekeeping, batch analytics, file processing.
75. Архітектурний борг, який уже видно
Історичний розвиток залишив місця, які не повністю відповідають нинішнім bounded contexts. Найочевидніший — Prom.ua у TCU.
Можуть існувати й інші прямі references на зовнішні proxy у WinForms-клієнті або data layer, які з'являлися поступово.
Кожну таку залежність корисно перевіряти питанням: чи є ця зовнішня система частиною предметної відповідальності TCU, чи TCU лише історично стала місцем, де інтеграцію було найшвидше реалізувати?
76. Цільова архітектурна картина TCU
┌───────────────────┐
│ Tcu5Client │
└─────────┬─────────┘
│
DataSources/Proxy
│
┌─────────▼─────────┐
│ Tcu5Server │
│ business logic │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ company TCU DB │
│ documents/moves │
│ parties/current │
└─────────▲─────────┘
│
┌─────────┴─────────┐
│ Tcu5CycleServer │
│ jobs/background │
└───────────────────┘
TCU ↔ Shopserver ↔ front-office / e-commerce
TCU ↔ TCC ↔ CRM / loyalty
TCU ↔ Billing ↔ subscription / service account context
Зовнішні системи повинні інтегруватися через відповідні bounded contexts, а не розширювати TCU до нескінченності.
77. Головні архітектурні рішення Trade Control Utility
- Документ є носієм господарської операції.
- Проведення та відкат є симетричними domain operations.
- Історія руху і поточний стан розділені.
- Партія є частиною собівартості та простежуваності.
- Підрозділ є універсальною точкою обліку.
- Клієнт не працює напряму з database.
- Background processing має окремий host.
- Multi-tenancy ізольована на рівні окремих company databases.
- Інтеграції не повинні розмивати предметну відповідальність TCU.
- Денормалізований поточний стан допустимий, якщо він відтворюється з історії.
78. Як пояснити TCU новому розробнику одним абзацом
Trade Control Utility — це багатокористувацька клієнт-серверна облікова система, де реальні господарські операції моделюються поліморфними документами. Проведення документа на сервері створює рухи товарів або коштів, змінює матеріалізований поточний стан і зберігає зв'язки з партіями, підрозділами, користувачами та іншими документами. WinForms-клієнт працює із сервером через DataSources і Proxy-контракти, а довготривалі та планові процеси винесені в окремий CycleServer. TCU є back-office source of truth для внутрішнього обліку, тоді як front-office, CRM та e-commerce інтеграції поступово відокремлюються в Shopserver, TCC та інші спеціалізовані сервіси.
79. Як цей матеріал корисно використовувати AI-асистенту
Матеріал навмисно містить повторення ключових понять у різних контекстах. Це корисно після chunking.
Окремий chunk про InventoryRecord повинен залишатися зрозумілим без попереднього chunk про GoodsItem. Окремий chunk про Shopserver integration повинен сам пояснювати, чому front-office container перетворюється на звичайні TCU documents. Окремий chunk про CycleServer повинен пояснювати, чому scheduled task не виконується у WinForms-клієнті.
У результаті semantic retrieval може знаходити відповіді не лише на «де кнопка», а й на «чому система так побудована».
80. Пов'язані матеріали
- Облік товарів у Trade Control Utility
- Облік фінансів у Trade Control Utility
- Довідник товарів
- Картка обліку товару на складі
- Довідник підрозділів
- Прибуткова накладна
- Видаткова накладна
- Накладна на передачу товару
- Касовий чек, накладна та прив'язка оплат
- Технологічна карта та облік виробництва
- Робота з цінами у Trade Control Utility
- Закриття звітного періоду
- Призначені завдання
- Архітектура сервісів Base2Base
- Shopserver. Схема обміну даними
---
Матеріал сформовано на підставі наданої кодової бази TCU (Tcu5Client.sln, Tcu5Server.sln, Tcu5CycleServer.sln) та документації Base2Base. Історичні елементи реалізації відділено від цільової архітектури там, де такий напрямок був явно визначений.
Andriy Kravchenko
Admin, Writer, File Uploader
Останнє оновлення:
8/21/2026 9:14:27 PM
6