gendesign/tradein-mvp/backend/app/services/brand.py
lekss361 02267d0306 feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in)
- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest

Первый запуск на сервере (вручную, один раз):
  - создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
  - docker network create gendesign_shared (если нет)
  - docker compose -p gendesign-tradein up -d
  - docker compose -p gendesign exec caddy caddy reload
2026-05-21 00:25:39 +03:00

66 lines
1.7 KiB
Python

"""Brand resolver — multi-tenant white-label.
Читает бренд по slug из `brands` Postgres-таблицы.
Используется PDF exporter + frontend (через /api/v1/brand/{slug} endpoint).
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from sqlalchemy import text
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
@dataclass(frozen=True, slots=True)
class Brand:
slug: str
name: str
logo_url: str | None
primary_color: str
accent_color: str
footer_text: str | None
pdf_disclaimer: str | None
_DEFAULT = Brand(
slug="generic",
name="Trade-In Estimator",
logo_url=None,
primary_color="#1d4ed8",
accent_color="#f59e0b",
footer_text=None,
pdf_disclaimer=None,
)
def get_brand(slug: str | None, db: Session) -> Brand:
"""Возвращает Brand по slug. Fallback на 'generic' если не найден."""
if not slug:
return _DEFAULT
row = db.execute(
text(
"""
SELECT slug, name, logo_url, primary_color, accent_color,
footer_text, pdf_disclaimer
FROM brands
WHERE slug = :slug
"""
),
{"slug": slug.lower()},
).fetchone()
if row is None:
logger.info("brand not found: %s — using generic", slug)
return _DEFAULT
return Brand(
slug=row.slug,
name=row.name,
logo_url=row.logo_url,
primary_color=row.primary_color or _DEFAULT.primary_color,
accent_color=row.accent_color or _DEFAULT.accent_color,
footer_text=row.footer_text,
pdf_disclaimer=row.pdf_disclaimer,
)