From e169a911463945902857a5c0895a1f33fc515850 Mon Sep 17 00:00:00 2001 From: mguschin Date: Tue, 12 May 2026 23:04:23 +0300 Subject: [PATCH] feat: /sync settings page with price multiplier and description postfix Adds price_multiplier and description_postfix to SyncConfig. The sync page at /sync lets users configure them. vk_sync reads these per-user settings and applies the multiplier to price and appends the postfix as "(postfix)" to the VK product description. Co-Authored-By: Claude Sonnet 4.6 --- web/main.py | 2 + .../0008_sync_config_price_postfix.py | 18 +++++ web/models/connections.py | 2 + web/routes/sync.py | 75 +++++++++++++++++++ web/tasks/vk_sync.py | 16 ++-- web/templates/sync.html | 35 +++++++++ 6 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 web/migrations/versions/0008_sync_config_price_postfix.py create mode 100644 web/routes/sync.py create mode 100644 web/templates/sync.html diff --git a/web/main.py b/web/main.py index 5c02e17..b52f3f0 100644 --- a/web/main.py +++ b/web/main.py @@ -39,6 +39,7 @@ from web.routes.catalog import router as catalog_router # noqa: E402 from web.routes.connections import router as connections_router # noqa: E402 from web.routes.vk_catalog import router as vk_catalog_router # noqa: E402 from web.routes.logs import router as logs_router # noqa: E402 +from web.routes.sync import router as sync_router # noqa: E402 app.include_router(auth_router) app.include_router(reset_router) @@ -50,6 +51,7 @@ app.include_router(catalog_router) app.include_router(connections_router) app.include_router(vk_catalog_router) app.include_router(logs_router) +app.include_router(sync_router) # ── Catalog redirect ───────────────────────────────────────────────────────── diff --git a/web/migrations/versions/0008_sync_config_price_postfix.py b/web/migrations/versions/0008_sync_config_price_postfix.py new file mode 100644 index 0000000..1165a51 --- /dev/null +++ b/web/migrations/versions/0008_sync_config_price_postfix.py @@ -0,0 +1,18 @@ +"""Add price_multiplier and description_postfix to sync_configs.""" +revision = "0008" +down_revision = "0007" +branch_labels = None +depends_on = None + +import sqlalchemy as sa +from alembic import op + + +def upgrade(): + op.add_column("sync_configs", sa.Column("price_multiplier", sa.Numeric(10, 4), nullable=False, server_default="1.0")) + op.add_column("sync_configs", sa.Column("description_postfix", sa.String(255), nullable=True)) + + +def downgrade(): + op.drop_column("sync_configs", "description_postfix") + op.drop_column("sync_configs", "price_multiplier") diff --git a/web/models/connections.py b/web/models/connections.py index c511b82..6f8a066 100644 --- a/web/models/connections.py +++ b/web/models/connections.py @@ -58,6 +58,8 @@ class SyncConfig(Base): user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) is_enabled = Column(Boolean, nullable=False, default=False) confirmed_at = Column(DateTime, nullable=True) + price_multiplier = Column(Numeric(10, 4), nullable=False, default=1.0) + description_postfix = Column(String(255), nullable=True) created_at = Column(DateTime, nullable=False, server_default=func.now()) updated_at = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now()) diff --git a/web/routes/sync.py b/web/routes/sync.py new file mode 100644 index 0000000..ff4d0fb --- /dev/null +++ b/web/routes/sync.py @@ -0,0 +1,75 @@ +"""Sync settings page.""" +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import RedirectResponse + +from sqlalchemy.orm import Session + +from web.auth.session import get_current_user +from web.config import settings +from web.database import get_db +from web.models.connections import SyncConfig +from web.templates_env import templates + +router = APIRouter() + + +def _render(request: Request, ctx: dict): + ctx["request"] = request + ctx.setdefault("jivosite_widget_id", settings.JIVOSITE_WIDGET_ID) + return templates.TemplateResponse(ctx.pop("request"), "sync.html", ctx) + + +def _now(): + return datetime.now(timezone.utc).replace(tzinfo=None) + + +@router.get("/sync") +async def sync_get(request: Request, db: Session = Depends(get_db)): + try: + user = get_current_user(request, db) + except Exception: + return RedirectResponse("/login", 303) + + config = db.query(SyncConfig).filter_by(user_id=user.id).first() + return _render(request, { + "user": user, + "config": config, + "saved": request.query_params.get("saved"), + }) + + +@router.post("/sync/settings") +async def sync_settings_post(request: Request, db: Session = Depends(get_db)): + try: + user = get_current_user(request, db) + except Exception: + return RedirectResponse("/login", 303) + + form = await request.form() + raw_multiplier = str(form.get("price_multiplier", "1")).strip() + postfix = str(form.get("description_postfix", "")).strip() or None + + try: + multiplier = float(raw_multiplier) + if multiplier <= 0: + raise ValueError + except ValueError: + multiplier = 1.0 + + config = db.query(SyncConfig).filter_by(user_id=user.id).first() + if config: + config.price_multiplier = multiplier + config.description_postfix = postfix + else: + config = SyncConfig( + user_id=user.id, + is_enabled=False, + price_multiplier=multiplier, + description_postfix=postfix, + ) + db.add(config) + + db.commit() + return RedirectResponse("/sync?saved=1", 303) diff --git a/web/tasks/vk_sync.py b/web/tasks/vk_sync.py index e0e826e..6a71c97 100644 --- a/web/tasks/vk_sync.py +++ b/web/tasks/vk_sync.py @@ -148,10 +148,13 @@ def _sync_product( album_id: str, vk_group_id: str, token: str, + sync_config=None, ) -> None: name = _name_for_vk(product.name) - price_rubles = _calc_price(product.price) - desc = product.name + multiplier = float(sync_config.price_multiplier) if sync_config and sync_config.price_multiplier else 1.0 + price_rubles = _calc_price(product.price) * multiplier + postfix = (sync_config.description_postfix or "").strip() if sync_config else "" + desc = f"{product.name} ({postfix})" if postfix else product.name stock = settings.VK_STOCK_AMOUNT if product.allow_to_sell else 0 owner_id = f"-{vk_group_id}" now = _now() @@ -257,11 +260,11 @@ def _sync_product( logger.info("user=%s created VK product '%s' id=%s", user_id, name, vk_item_id) -def _sync_product_list(db, user_id, products, album_id, vk_group_id, token, results, owned_ids): +def _sync_product_list(db, user_id, products, album_id, vk_group_id, token, results, owned_ids, sync_config=None): for product in products: was_new = product.vk_product_id is None try: - _sync_product(db, user_id, product, album_id, vk_group_id, token) + _sync_product(db, user_id, product, album_id, vk_group_id, token, sync_config) if product.vk_product_id: owned_ids.add(product.vk_product_id) if product.synced_at: @@ -318,6 +321,7 @@ def _sync_user(db, user_id: int, token: str, vk_group_id: str) -> dict: results = {"created": 0, "updated": 0, "skipped": 0, "deleted": 0, "errors": 0} owned_ids: set[str] = set() # VK product IDs that Evotor owns this run + sync_config = db.query(SyncConfig).filter_by(user_id=user_id).first() enabled_stores = _enabled_store_ids(db, user_id) stores = db.query(CachedStore).filter_by(user_id=user_id).all() @@ -348,7 +352,7 @@ def _sync_user(db, user_id: int, token: str, vk_group_id: str) -> dict: products = db.query(CachedProduct).filter_by( user_id=user_id, store_evotor_id=store.evotor_id, group_evotor_id=group.evotor_id, ).all() - _sync_product_list(db, user_id, products, album_id, vk_group_id, token, results, owned_ids) + _sync_product_list(db, user_id, products, album_id, vk_group_id, token, results, owned_ids, sync_config) # Ungrouped products → "Без категории" album ungrouped = db.query(CachedProduct).filter_by( @@ -361,7 +365,7 @@ def _sync_user(db, user_id: int, token: str, vk_group_id: str) -> dict: logger.error("user=%s ensure_album failed for 'Без категории': %s", user_id, e) fallback_album_id = None if fallback_album_id: - _sync_product_list(db, user_id, ungrouped, fallback_album_id, vk_group_id, token, results, owned_ids) + _sync_product_list(db, user_id, ungrouped, fallback_album_id, vk_group_id, token, results, owned_ids, sync_config) # Delete VK products not owned by any Evotor product _delete_orphans(db, user_id, vk_group_id, owned_ids, token, results) diff --git a/web/templates/sync.html b/web/templates/sync.html new file mode 100644 index 0000000..1f2fab9 --- /dev/null +++ b/web/templates/sync.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %}Синхронизация — ЭВОСИНК{% endblock %} + +{% block content %} +
+

Синхронизация

+
+ +{% if saved %} + +{% endif %} + +
+

Настройки цены и описания

+
+
+ + +
+ +
+
+{% endblock %}