ESWF — Інструкція: Реєстрація та Активація Платного Плагіну¶
Документ описує повний цикл від нуля до робочого плагіну в DOP-інтерфейсі.
Versioning baseline (2026-04-28). Кожен
Product(backend) іSectionItem(frontendapplications.ts) має поляversion(default'0.1.0') іchannel(default'dev'). Поки всі додатки у dev-каналі — AppStore не показує селектор каналу. Промоція уbeta/stable— Phase 2 (update-delivery.md). Поточна core-версія зчитується черезGET /api/v1/system/version/(single source of truth —backend/eswf/__version__.py). Дерево залежностей (requires+forModules) — централізовано вfrontend/erp/src/config/dependencies.ts.
Два рівні гейту¶
У платного плагіну два незалежні фільтри. Регресія будь-якого ламає або app store, або sidebar — тому обидва покриті автотестами.
| Рівень | Що гейтує | Хто керує | Перевіряє |
|---|---|---|---|
| Static (backend package) | Чи взагалі зареєстрований URL /api/v1/<plugin>/ |
ESWF_PLUGINS env + pip install eswf-<plugin> |
apps.is_installed('<plugin>') у eswf/urls.py |
| Dynamic (tenant license) | Чи tenant має активну ліцензію | shop.License(tenant, product, is_active) |
GET /api/v1/shop/licenses/active-plugins/ |
Static: _PLUGIN_URLS + apps.is_installed¶
_PLUGIN_URLS = {
'logistic': ('api/v1/logistic/', 'logistic.urls'),
'containerhub': ('api/v1/containerhub/', 'containerhub.urls'),
'essentials_quality': ('api/v1/essentials-quality/', 'essentials_quality.urls'),
}
for app_label, (prefix, urls_module) in _PLUGIN_URLS.items():
if apps.is_installed(app_label):
urlpatterns.append(path(prefix, include(urls_module)))
Плагін реально встановлено (його код є у sys.path + app у INSTALLED_APPS)
тільки якщо ESWF_PLUGINS env var його включає. Поточне dev-середовище:
ESWF_PLUGINS="*" → всі відомі плагіни активні (і logistic встановлено
через pip editable з plugins/eswf-logistic/).
У community-збірці без paid-плагінів URL просто не реєструється —
клієнт отримує 404 на /api/v1/logistic/ замість 500.
Dynamic: active-plugins endpoint → useActivePlugins()¶
Навіть якщо backend app встановлений, пункт sidebar прихований доки у tenant немає активної ліцензії:
shop/views.py→LicenseViewSet.active_plugins→/api/v1/shop/licenses/active-plugins/useActivePlugins.ts→useFilteredSections()→ ховає item якщоitem.pluginвідсутній у відповіді.
Regression guard — автотести¶
Покриття Phase 1 (audit 2026-04-21):
backend/core/tests/test_plugin_exclusion.py— структура_PLUGIN_URLS, синхронність зESWF_AVAILABLE_PLUGINS, correlation міжapps.is_installed()та наявністю URL-роута, контракт відповідіactive-plugins(список непустих рядків).
Концепція¶
Backend DB Frontend Config DOP Interface
──────────────── ────────────────── ──────────────────
Product.code ──────► applications.ts App Store card
"appXXX" item.code = "appXXX" "Install / Activate"
item.price = "paid"
│
Product.plugin_key ────────────►essentials.ts (etc.) Navigation item
"xxx_exchange" item.plugin = "xxx_exchange" (visible after activation)
│
ActivationCode.code │
UUID ──────────────── User enters in modal
│
License(tenant, product) ───────► /active-plugins/ → item shown in nav
Три правила зв'язку:
1. Product.code = SectionItem.code в applications.ts
2. Product.plugin_key = SectionItem.plugin в config-файлі модуля
3. Product.pricing = 'paid' → потрібен ActivationCode
Крок 1 — Реєстрація продукту в Django Admin¶
1.1 Відкрити Django Admin¶
1.2 Заповнити поля¶
| Поле | Значення | Коментар |
|---|---|---|
| Name | M.E.Doc Integration |
Назва, що показується в App Store |
| Name UA | Інтеграція з M.E.Doc |
Переклад |
| Slug | medoc-integration |
URL-friendly, унікальний |
| Code | appMED |
Критично: має збігатися з item.code в applications.ts |
| Pricing | paid |
free / paid / subscription |
| Plugin type | builtin |
Код вже є в репозиторії |
| Plugin key | medoc_exchange |
Критично: має збігатися з item.plugin в config-файлі секції |
| Status | available |
|
| Price | 49.00 |
Для відображення (опційно) |
Правило:
Product.code→ відповідає картці в App Store. Правило:Product.plugin_key→ відповідає пункту меню в DOP.
1.3 Альтернатива: через Django shell¶
from shop.models import Product
Product.objects.create(
name='M.E.Doc Integration',
name_ua='Інтеграція з M.E.Doc',
slug='medoc-integration',
code='appMED', # ← = item.code в applications.ts
pricing='paid',
plugin_type='builtin',
plugin_key='medoc_exchange', # ← = item.plugin в essentials.ts
status='available',
description='Export goods shipments to M.E.Doc XML for DPS Ukraine.',
)
Крок 2 — Frontend: картка в App Store (applications.ts)¶
Файл: frontend/erp/src/config/applications.ts
Знайти subgroup subgroupsapps і додати картку:
{
code: "appMED", // ← = Product.code в Django
name: "M.E.Doc Integration",
name_ua: "Інтеграція з M.E.Doc",
type: ItemType.APPLICATION,
icon: "FileDoneOutlined",
hierarchy: false,
description: "Export goods shipments to M.E.Doc XML format (J1203001/F1203001) for tax reporting.",
description_ua: "Експорт видаткових накладних у формат M.E.Doc для подачі до ДПС.",
formType: "default",
showInSidebar: false,
showInMenu: true,
allowedRoles: [],
printForms: [],
subtables: [],
status: "available",
price: "paid", // ← показує кнопку "Activate" замість "Install"
} as SectionItem,
Увага:
price: "paid"у frontend-конфізі — тільки для відображення кнопки. Реальна перевірка платності —Product.pricingв Django.
Крок 3 — Frontend: пункт меню в секції (essentials.ts або інший)¶
Файл: frontend/erp/src/config/essentials.ts
(або fleet.ts, logistic.ts — залежно від модуля)
Додати item в потрібну subgroup (наприклад, Processes → Financial Processes):
{
code: "medocExport", // код маршруту (itemCode в URL)
name: "M.E.Doc Export",
name_ua: "Експорт до M.E.Doc",
type: ItemType.PROCESS, // або MASTERDATA, TRANSACTIONDATA тощо
icon: "FileDoneOutlined",
hierarchy: false,
description: "Export goods shipments to M.E.Doc XML format.",
description_ua: "Експорт видаткових накладних у формат M.E.Doc.",
formType: "default",
showInSidebar: false,
showInMenu: true,
allowedRoles: [],
printForms: [],
subtables: [],
plugin: "medoc_exchange", // ← = Product.plugin_key в Django
// item прихований якщо ліцензія неактивна
} as SectionItem,
Механізм видимості:
useFilteredSections()→useActivePlugins()→GET /api/v1/shop/licenses/active-plugins/Якщо"medoc_exchange"є у відповіді → item показується. Інакше — прихований.
Крок 4 — Підключення компонента до ProcessPanel¶
Якщо type: ItemType.PROCESS — потрібно додати case в:
Файл: frontend/erp/src/components/Process/ProcessPanel.tsx
import { MedocExportPage } from '@/components/Essentials/MedocExportPage';
// у switch:
case 'medocExport':
return <MedocExportPage />;
Аналогічно для інших типів: -
MASTERDATA→ автоматично черезMasterDataPage+entityCode-TRANSACTIONDATA→ автоматично черезTransactionPage+transactionType-JOURNAL/LEDGER/REPORT→ автоматично
Крок 5 — Генерація кодів активації¶
5.1 Через Django Admin¶
| Поле | Значення |
|---|---|
| Product | M.E.Doc Integration |
| Code | (UUID генерується автоматично) |
| Is used | ☐ (не відмічати) |
| Expires at | порожнє = безстроковий |
| Note | Наприклад: "Client: ТОВ Ромашка, Order #42" |
Після збереження — скопіювати UUID зі сторінки перегляду.
5.2 Через Django shell (batch-генерація)¶
from shop.models import Product, ActivationCode
product = Product.objects.get(code='appMED')
# Генерувати N кодів
codes = [ActivationCode.objects.create(product=product, note=f'Order #{i}') for i in range(1, 6)]
for c in codes:
print(c.code) # виводить UUID
5.3 Через shell з фіксованим UUID (для тестів)¶
import uuid
from shop.models import Product, ActivationCode
product = Product.objects.get(code='appMED')
ActivationCode.objects.get_or_create(
code=uuid.UUID('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'),
product=product,
defaults={'is_used': False}
)
Тестовий код:
aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
Крок 6 — Активація плагіну в DOP¶
-
Відкрити Applications → App Store URL:
/applications/groupsapplications/appstore -
Знайти картку плагіну (фільтр Available або All)
-
Натиснути Activate (помаранчева кнопка з іконкою ключа)
-
У модальному вікні "Plugin Activation" ввести UUID-код:
-
Натиснути Activate → повідомлення "M.E.Doc Integration installed"
-
Картка переходить у стан Installed (зелений бейдж)
-
У навігаційному меню з'являється: Essentials → Processes → Financial Processes → M.E.Doc Export
Крок 7 — Перевірка¶
Backend перевірка через shell¶
from shop.models import License, Product
product = Product.objects.get(code='appMED')
license = License.objects.get(product=product)
print('is_active:', license.is_active)
print('tenant:', license.tenant)
print('used_code:', license.activation_code)
API перевірка¶
# Отримати список активних plugin_key для тенанту
curl -H "Authorization: Bearer <access_token>" \
http://localhost:8000/api/v1/shop/licenses/active-plugins/
# Очікувана відповідь:
["bank_exchange", "medoc_exchange"]
Swagger¶
Якщо "medoc_exchange" присутній — пункт меню видимий у DOP.
Деактивація (Uninstall)¶
- App Store → картка плагіну → Uninstall (червона кнопка)
- Backend:
License.is_active = False(soft delete) - Frontend:
active-pluginsбільше не містить"medoc_exchange" - Пункт меню зникає з навігації
Повторна активація після uninstall:
- Потрібен новий ActivationCode (попередній помічений is_used=True)
- Або через shell: license.is_active = True; license.save()
Чеклист для нового плагіну¶
Backend:
[ ] Product.code = відповідає item.code в applications.ts
[ ] Product.plugin_key = відповідає item.plugin в config секції
[ ] Product.pricing = 'paid'
[ ] Product.plugin_type = 'builtin'
[ ] ActivationCode створений для продукту
Frontend config:
[ ] applications.ts — картка з code="appXXX", price="paid"
[ ] essentials.ts (або інший) — item з plugin="xxx_key"
Frontend component:
[ ] Компонент створений (якщо PROCESS-тип)
[ ] Case додано в ProcessPanel.tsx
Тест:
[ ] App Store показує картку
[ ] Кнопка "Activate" відкриває модаль
[ ] UUID-код приймається → License створено
[ ] active-plugins повертає plugin_key
[ ] Пункт меню з'являється в навігації
[ ] Endpoint повертає 200 (не 403) після активації
Схема зв'язків¶
applications.ts Django Admin / Shell
─────────────── ─────────────────────────────────────────
item.code ────────────► Product.code (унікальний, max 50)
item.price = "paid" Product.pricing = 'paid'
Product.plugin_key ──► essentials.ts item.plugin
Product.status = 'available'
│
ActivationCode
.product = Product
.code = UUID ← надається клієнту
.is_used = False
.expires_at = None
│ (клієнт вводить UUID в App Store)
▼
License
.tenant = поточний тенант
.product = Product
.is_active = True
.activation_code → is_used = True
│
▼
GET /api/v1/shop/licenses/active-plugins/
["medoc_exchange", ...]
│
useFilteredSections() → item видимий