Перейти до змісту

ESWF — Інструкція: Реєстрація та Активація Платного Плагіну

Документ описує повний цикл від нуля до робочого плагіну в DOP-інтерфейсі.

Versioning baseline (2026-04-28). Кожен Product (backend) і SectionItem (frontend applications.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

backend/eswf/urls.py:

_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 немає активної ліцензії:

  1. shop/views.pyLicenseViewSet.active_plugins/api/v1/shop/licenses/active-plugins/
  2. useActivePlugins.tsuseFilteredSections() → ховає 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

http://localhost:8000/admin/
Shop → Products → Add Product

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

cd backend
python manage.py 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

Admin → Shop → Activation Codes → Add Activation Code
Поле Значення
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

  1. Відкрити Applications → App Store URL: /applications/groupsapplications/appstore

  2. Знайти картку плагіну (фільтр Available або All)

  3. Натиснути Activate (помаранчева кнопка з іконкою ключа)

  4. У модальному вікні "Plugin Activation" ввести UUID-код:

    aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
    

  5. Натиснути Activate → повідомлення "M.E.Doc Integration installed"

  6. Картка переходить у стан Installed (зелений бейдж)

  7. У навігаційному меню з'являється: 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

GET /api/v1/shop/licenses/active-plugins/

Якщо "medoc_exchange" присутній — пункт меню видимий у DOP.


Деактивація (Uninstall)

  1. App Store → картка плагіну → Uninstall (червона кнопка)
  2. Backend: License.is_active = False (soft delete)
  3. Frontend: active-plugins більше не містить "medoc_exchange"
  4. Пункт меню зникає з навігації

Повторна активація після 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 видимий