Catalog, VK catalog, and VK sync tasks were querying all connections regardless of user role. Admin and system accounts with stored tokens were generating unnecessary Evotor and VK API calls. Now all three tasks join to the users table and filter role = 'user' only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EvoSync
Web service for syncing a product catalog from Evotor POS → VK Market. Users connect their Evotor account and a VK community; products from the cash register then appear automatically in the VK store.
Architecture
┌─────────────────────────────────────────┐
│ Docker Compose │
│ │
Browser / Evotor ─────► web :8000 (FastAPI + Uvicorn) │
:8080 │ │ │
│ ├── MariaDB :3306 (primary DB) │
│ ├── Redis :6379 (Celery broker) │
│ │ │
│ ├── worker (Celery worker) │
│ ├── beat (Celery beat scheduler) │
│ └── flower :5555 (queue monitor) │
└─────────────────────────────────────────┘
Services
| Service | Image / Dockerfile | Purpose | External port |
|---|---|---|---|
web |
Dockerfile.web |
FastAPI app, runs Alembic migrations on start | 8080 → 8000 |
worker |
Dockerfile.web |
Celery worker (sync, health, notifications…) | — |
beat |
Dockerfile.web |
Celery Beat — periodic task scheduler | — |
flower |
Dockerfile.web |
Celery queue monitoring UI | 5555 |
db |
mariadb:11.4 |
Primary relational database | — |
redis |
redis:7-alpine |
Celery broker and result backend | — |
Stack
- Python 3.12, FastAPI 0.115, Uvicorn
- SQLAlchemy 2 + Alembic, MariaDB (PyMySQL)
- Celery 5 + Redis — background tasks, periodic catalog sync
- Jinja2 — server-side HTML rendering (
web/templates/) - Pydantic Settings — configuration from env vars /
.env - bcrypt — password hashing
- python-json-logger — structured JSON logs to stdout
Database Schema
| Table | Purpose |
|---|---|
users |
User accounts (roles: system / admin / user; statuses: pending / active / suspended) |
evotor_connections |
User ↔ Evotor link (access_token, api_token returned to Evotor webhooks) |
vk_connections |
User ↔ VK link (user access token + VK community ID) |
sync_configs |
Per-user sync settings |
sync_filters |
Store / group inclusion filters (entity_type: store / group) |
cached_stores |
Cached list of Evotor stores |
cached_groups |
Cached Evotor product groups |
cached_products |
Cached Evotor products; vk_product_id stores the VK market item ID after first push |
vk_cached_albums |
Cached VK Market albums (product groups) |
vk_cached_products |
Cached VK Market products |
roles |
RBAC roles |
permissions |
RBAC permissions |
role_permissions |
M2M: role ↔ permission |
user_roles |
M2M: user ↔ role |
Background Tasks
Periodic tasks run via Celery Beat and are executed by the worker service.
Beat fires a single sync pipeline every CATALOG_REFRESH_INTERVAL_SECONDS. The three tasks run as a Celery chain — each step starts only after the previous one completes:
run_sync_pipeline (beat entry)
└─► refresh_catalog — fetch Evotor stores / groups / products
└─► refresh_vk_catalog — fetch VK Market albums / products
└─► mirror_to_vk — push Evotor → VK
| Task | Queue | Description |
|---|---|---|
web.tasks.celery_app.run_sync_pipeline |
default | Beat entry point; dispatches the chain |
web.tasks.catalog.refresh_catalog |
default | Fetches Evotor catalog for all connected users; upserts cached_stores, cached_groups, cached_products |
web.tasks.vk_catalog.refresh_vk_catalog |
default | Fetches VK Market albums and products for all connected users; upserts vk_cached_albums, vk_cached_products |
web.tasks.vk_sync.mirror_to_vk |
sync | Mirrors enabled Evotor groups/products → VK Market (create or conditional update) |
Evotor fetch sequence per user:
GET /stores→ upsertcached_stores- For each store:
GET /stores/{id}/product-groups→ upsertcached_groups - For each store:
GET /stores/{id}/products→ upsertcached_products
VK fetch sequence per user:
market.getAlbums→ upsertvk_cached_albumsmarket.get(extended=1, paginated) → upsertvk_cached_productswith album membership
Mirror logic per user (mirror_to_vk):
- Skips stores not in enabled store filters; skips groups not in enabled group filters
- For each enabled group: ensures a matching VK album exists (creates via
market.addAlbumif missing) - For each product in the group:
- Create (
allow_to_sell=true, novk_product_idyet): uploads default photo once per run, callsmarket.add, assigns to album, stores returnedvk_product_id - Update (has
vk_product_id): callsmarket.editonly if name, price, description, or stock_amount changed vs the cached VK state - Skip: product disabled and never pushed, or nothing changed
- Create (
- Price is multiplied by the per-user
price_multiplierfromsync_configs(configurable on the/syncpage) - Description is built as
"Name (цена за N unit.)"when the product has ameasure_name
Per-user failures are logged and skipped — one broken token does not block other users.
Evotor stores that return 402 Payment Required (subscription limit) are silently skipped at debug log level.
Routes
Connections
| Method | Path | Description |
|---|---|---|
| GET | /connections |
View Evotor and VK connection status |
| POST | /connections/evotor |
Save / update Evotor API token manually |
| POST | /connections/evotor/disconnect |
Remove Evotor connection |
| POST | /connections/evotor/test |
Test Evotor connection (JSON) |
| POST | /connections/vk |
Save / update VK token and group ID |
| POST | /connections/vk/disconnect |
Remove VK connection |
| POST | /connections/vk/test |
Test VK connection (JSON) |
Public / Authentication
| Method | Path | Description |
|---|---|---|
| GET | / |
Redirects to /profile or /login |
| GET | /health |
Health check (JSON) |
| GET/POST | /register |
User registration |
| GET | /confirm-email |
Email confirmation via token |
| GET | /resend-confirm |
Resend confirmation email |
| GET/POST | /login |
Login |
| GET | /logout |
Logout |
| GET/POST | /forgot-password |
Request password reset |
| GET/POST | /reset-password |
Reset password via token |
| GET/POST | /invite |
Complete registration via invite link |
Profile (requires session)
| Method | Path | Description |
|---|---|---|
| GET | /profile |
View profile |
| GET/POST | /profile/edit |
Edit profile |
| GET/POST | /profile/change-password |
Change password |
| GET/POST | /profile/delete |
Delete account |
Admin panel (/admin, roles: admin / system)
| Method | Path | Description |
|---|---|---|
| GET | /admin/users |
User list |
| GET | /admin/users/{id} |
User detail |
| POST | /admin/users/{id}/activate |
Activate user |
| POST | /admin/users/{id}/suspend |
Suspend user |
| POST | /admin/users/{id}/reset-password |
Reset user password |
| POST | /admin/users/{id}/send-invite |
Send invite email |
| POST | /admin/users/{id}/edit |
Edit user data |
| POST | /admin/users/{id}/delete |
Delete user |
| GET | /admin/roles |
Roles and permissions |
| POST | /admin/roles/{id}/permissions |
Update role permissions |
Evotor Webhooks (Bearer EVOTOR_WEBHOOK_SECRET)
| Method | Path | Description |
|---|---|---|
| POST | /user/create |
Evotor creates/links a user; returns api_token |
| POST | /user/verify |
Evotor verifies user credentials; returns api_token |
| POST | /user/token |
Evotor delivers its own access_token for a user |
Evotor Catalog (requires session)
| Method | Path | Description |
|---|---|---|
| GET | /catalog |
Redirects to /catalog/stores |
| GET | /catalog/stores |
Evotor stores with per-store sync toggle |
| GET | /catalog/stores/{id}/groups |
Product groups with per-group sync toggle |
| GET | /catalog/stores/{id}/products |
Products (filterable by group) |
| POST | /catalog/stores/{id}/toggle |
Enable / disable store sync |
| POST | /catalog/stores/{id}/groups/{gid}/toggle |
Enable / disable group sync |
Sync Settings (requires session)
| Method | Path | Description |
|---|---|---|
| GET | /sync |
Sync settings: task on/off switches, price multiplier |
| POST | /sync/settings |
Save sync settings |
VK Catalog (requires session)
| Method | Path | Description |
|---|---|---|
| GET | /vk-catalog/albums |
VK Market albums (product groups) |
| GET | /vk-catalog/albums/{id}/products |
Products in a VK album |
Admin Logs (/admin, roles: admin / system)
| Method | Path | Description |
|---|---|---|
| GET | /admin/logs |
API request/response log viewer with filters and pagination |
API Docs
| Path | Description |
|---|---|
/docs |
Swagger UI |
/redoc |
ReDoc |
Configuration
All settings are read from environment variables or a .env file:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
mysql+pymysql://…@db:3306/evosync |
MariaDB connection string |
REDIS_URL |
redis://redis:6379/0 |
Redis connection string |
SECRET_KEY |
change-me-in-production |
Session signing key |
DOMAIN |
— | Public domain name (e.g. example.com); used to derive BASE_URL and nginx config |
BASE_URL |
https://${DOMAIN} |
Public URL of the service |
EVOTOR_APP_ID |
— | Evotor application ID |
EVOTOR_WEBHOOK_SECRET |
— | Bearer secret for webhook endpoints |
JIVOSITE_WIDGET_ID |
— | JivoSite widget ID |
VK_DEFAULT_PHOTO_PATH |
/app/default_product.png |
Fallback image path for VK products |
VK_API_VERSION |
5.199 |
VK API version |
CATALOG_REFRESH_INTERVAL_SECONDS |
3600 |
Sync pipeline interval in seconds |
VK_CATEGORY_ID |
40932 |
VK Market category ID for all products |
VK_STOCK_AMOUNT |
1000 |
Stock amount set for in-sale products |
INVITE_EXPIRE_HOURS |
48 |
Invite link TTL in hours |
EMAIL_PROVIDER |
console |
Email provider (console / smtp / …) |
SMS_PROVIDER |
console |
SMS provider |
FLOWER_USER / FLOWER_PASSWORD |
admin / changeme |
Basic Auth credentials for Flower |
Running
cp .env.example .env # set DOMAIN and other values
docker compose up -d --build
App is available at http://localhost:8080.
Flower (queue monitor) at http://localhost:5555.
First deploy (TLS)
Run once per domain (repeat for every domain pointing to this server):
# 1. Obtain TLS certificate
sudo ./scripts/init-letsencrypt.sh my-products.ru
sudo ./scripts/init-letsencrypt.sh мои-товары.рф
# 2. Generate nginx site config and symlink it into sites-enabled
sudo ./scripts/generate-nginx-conf.sh my-products.ru
sudo ./scripts/generate-nginx-conf.sh мои-товары.рф
# 3. Reload nginx
sudo systemctl reload nginx
generate-nginx-conf.sh expands nginx/nginx.conf.template with the given domain and writes the result to /etc/nginx/sites-available/<domain>.conf, then creates a symlink in sites-enabled.
Set up auto-renewal (if not already configured by certbot):
0 3 * * * root certbot renew --quiet && systemctl reload nginx
Development
pip install -r requirements.txt
alembic upgrade head
uvicorn web.main:app --reload --port 8000
Tests
pytest --cov=web