feat: VK OAuth flow, catalog sync improvements, and expanded test suite

- Add VK OAuth implicit flow: /vk-auth redirect, /vk-callback JS page,
  /vk-callback/save endpoint with state validation
- Add VK_CLIENT_ID/VK_CLIENT_SECRET to config
- Add refresh_token/token_expires_at columns to vk_connections (migration 0006)
- Fix vk_catalog task: handle price/thumb_photo as string or dict (VK API v5.199)
- Fix connections/vk/test: use groups.getById instead of market.getAlbums
  (works with both user and group tokens)
- Add orphan deletion to mirror_to_vk: VK products not in Evotor are removed
- Handle ungrouped Evotor products: push to "Без категории" VK album
- Respect SyncConfig.is_enabled in mirror_to_vk
- Add product count column to catalog groups page
- Add group name column to catalog products page
- Expand test suite: 73 new tests covering connections routes, catalog routes,
  vk_sync task logic, and catalog task helpers (138 total, all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mguschin
2026-05-12 15:09:47 +03:00
parent 4f4081c54c
commit 7b4f52b005
16 changed files with 1624 additions and 32 deletions

View File

@@ -23,6 +23,7 @@
<tr>
<th>Синхронизация</th>
<th>Название</th>
<th>Количество товаров</th>
<th>ID</th>
<th>Обновлено</th>
<th></th>
@@ -47,6 +48,7 @@
</form>
</td>
<td><i class="bi bi-folder2 me-1 text-muted"></i> <strong>{{ g.name }}</strong></td>
<td class="text-muted small">{{ product_counts.get(g.evotor_id, 0) }}</td>
<td class="text-muted small">{{ g.evotor_id }}</td>
<td class="text-muted small">{{ g.fetched_at | datefmt }}</td>
<td>

View File

@@ -40,6 +40,7 @@
<thead>
<tr>
<th>Название</th>
<th>Группа</th>
<th>Артикул</th>
<th>Цена</th>
<th>Остаток</th>
@@ -52,6 +53,7 @@
{% for p in products %}
<tr>
<td>{{ p.name }}</td>
<td class="text-muted small">{{ group_map.get(p.group_evotor_id) or '—' }}</td>
<td class="text-muted small">{{ p.article_number or '—' }}</td>
<td>{% if p.price is not none %}{{ p.price | price }}{% else %}—{% endif %}</td>
<td>{% if p.quantity is not none %}{{ p.quantity }}{% else %}—{% endif %}</td>

View File

@@ -134,14 +134,15 @@
{% endif %}
<div class="card-body">
<details {% if not vk %}open{% endif %}>
<summary>
{% if vk %}Обновить подключение{% else %}Подключить ВКонтакте{% endif %}
</summary>
<p class="text-muted small mt-2">
Укажите токен пользователя VK с правами <code>market,photos,groups</code>
и ID сообщества, в котором включён Маркет.
</p>
<div class="mt-3">
<a href="/vk-auth" role="button">
<i class="bi bi-box-arrow-in-right me-1"></i>
{% if vk %}Переподключить ВКонтакте{% else %}Войти через ВКонтакте{% endif %}
</a>
</div>
<details class="mt-3">
<summary class="text-muted small">Ввести токен вручную</summary>
<form method="post" action="/connections/vk" class="mt-2">
<label>
Токен доступа VK