- 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
66 lines
1.7 KiB
Python
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,
|
|
)
|