Merge pull request 'fix(tradein): удалить white-label бренд «Практика» целиком (БД + маппинг + ассеты)' (#1779) from fix/tradein-remove-praktika-brand into main
All checks were successful
Deploy Trade-In / test (push) Successful in 1m20s
Deploy Trade-In / build-frontend (push) Successful in 1m46s
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m16s
Deploy Trade-In / deploy (push) Successful in 1m0s

Reviewed-on: #1779
This commit is contained in:
lekss361 2026-06-19 10:09:38 +00:00
commit be3a5df8eb
7 changed files with 32 additions and 30 deletions

View file

@ -50,20 +50,14 @@ class UserScope(TypedDict):
# Account → brand mapping (#657 brand-by-account)
# ---------------------------------------------------------------------------
#
# Брендирование «Практика» (#657 brand-by-account) применяется ТОЛЬКО под
# аккаунтом `praktika`. Резолвится в /me как UserScope.brand, а при генерации
# trade-in PDF — по `created_by` оценки (анти-spoofing #7, бренд не хранится).
# Остальные юзеры (вкл. `kopylov`) → None (стандартный generic UI/PDF). Slug
# должен существовать в таблице `brands` (см. services/brand.py) — иначе
# useBrand fallback на generic.
#
# `kopylov` ранее мапился на бренд `praktika`, но это приводило к утечке лого
# «Практики» в отчёты, созданные под `kopylov`, не тем клиентам (инцидент:
# отчёт для Брусники с лого Практики) — маппинг удалён. NB: kopylov/praktika
# остаются в роли `pilot` в roles.yaml — это РОЛЬ, не бренд, не трогаем.
_USERNAME_BRAND: dict[str, str] = {
"praktika": "praktika",
}
# White-label бренд «Практика» снят целиком (бизнес-решение 2026-06-19): строка
# удалена из таблицы `brands` (миграция 121), SVG-логотип убран из фронта.
# Маппинг аккаунт→бренд теперь пустой — все юзеры резолвятся в None (generic
# UI/PDF). Механизм сохранён для будущих white-label клиентов: добавить slug
# сюда + строку в `brands` (см. services/brand.py). NB: аккаунты praktika/
# kopylov остаются пилот-логинами (роль `pilot` в roles.yaml) — это РОЛЬ, не
# бренд, не трогаем.
_USERNAME_BRAND: dict[str, str] = {}
def get_brand_for_user(username: str) -> str | None:

View file

@ -0,0 +1,9 @@
-- 121_remove_brand_praktika.sql
-- Полное удаление white-label бренда «Практика» (бизнес-решение 2026-06-19).
-- Бренд снят целиком: эта миграция убирает строку из brands; аккаунт-маппинг
-- praktika/kopylov удалён в app/core/auth.py; SVG-логотип удалён из фронта.
-- Идемпотентно: DELETE по PK slug. На fresh-build удаляет строку, засеянную 002
-- и обновлённую 084/099 (эта миграция с бОльшим номером — выполняется последней).
BEGIN;
DELETE FROM brands WHERE slug = 'praktika';
COMMIT;

View file

@ -1,7 +1,8 @@
"""Tests for #657 brand-by-account — UserScope.brand resolution.
Pure-function tests against app.core.auth: brand slug привязывается к аккаунту
(ТОЛЬКО praktika "praktika"), остальные юзеры включая kopylov None.
Pure-function tests against app.core.auth. White-label бренд «Практика» снят
целиком (бизнес-решение 2026-06-19): маппинг аккаунтбренд пустой, поэтому ВСЕ
юзеры (включая praktika/kopylov) резолвятся в None (generic UI/PDF).
get_user_scope прокидывает brand в scope, который /me отдаёт фронту.
Без БД и сети: читается реальный auth/roles.yaml (ancestor-walk внутри репо).
@ -28,10 +29,9 @@ from app.core.auth import get_brand_for_user, get_user_scope # noqa: E402
@pytest.mark.parametrize(
("username", "expected"),
[
("praktika", "praktika"),
# kopylov больше НЕ брендируется как Практика (#657 brand leak fix):
# бренд резолвится по created_by при генерации PDF, поэтому утечка лого
# Практики в отчёты под kopylov исправляется тем, что kopylov → generic.
# «Практика» снята целиком (2026-06-19): praktika/kopylov больше не
# брендируются — маппинг пустой, все аккаунты → generic UI/PDF.
("praktika", None),
("kopylov", None),
("admin", None),
("user1", None),
@ -43,15 +43,16 @@ def test_get_brand_for_user(username: str, expected: str | None) -> None:
def test_user_scope_brand_none_for_kopylov() -> None:
# Регресс-гард на #657 brand leak: kopylov-оценки рендерят generic PDF.
# Регресс-гард: бренд снят, kopylov-оценки рендерят generic PDF.
scope = get_user_scope("kopylov")
assert scope["brand"] is None
assert scope["username"] == "kopylov"
def test_user_scope_carries_brand_for_praktika() -> None:
def test_user_scope_brand_none_for_praktika() -> None:
# Бренд «Практика» удалён — praktika-аккаунт теперь generic.
scope = get_user_scope("praktika")
assert scope["brand"] == "praktika"
assert scope["brand"] is None
assert scope["username"] == "praktika"

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1498 194" fill="none" role="img" aria-label="Практика"><path d="M1497.3 80.77h-79.53l-32.75 112.282h23.38l88.9-112.282z" fill="#FF5BBA"/><path d="M1279.34 139.779c-22.35 0-59.83-5.658-59.83-43.524 0-34.767 39.58-42.067 72.78-42.067h40.58V51.01c0-17.81-14.88-21.467-37.23-21.467-15.95 0-51.49 3.906-61.53 5.037V5.704C1242.28 4.479 1274.05 0 1299.36 0c25.31 0 42.35 4.805 54.45 14.306 12.09 9.487 17.97 22.243 17.97 38.998v83.731h-15.72l-18.18-23.312-1.58 3.627c-2.7 6.2-8.71 11.517-17.87 15.81-9.4 4.402-22.55 6.634-39.09 6.634v-.015zm12.13-63.147c-8.17 0-33.04 0-33.04 18.243 0 8.37 5 18.352 28.81 18.352 12.4 0 22.76-2.666 30.75-7.936 7.92-5.208 14.88-17.5 14.88-26.319v-2.356h-41.4v.016zM113.367 136.911V32.101H38.905v104.81H0V3.642h152.272v133.269h-38.905zM1170.91 137.035l-41.52-52.452h-36.94v52.452h-38.92V3.642h38.92v52.452h36.94l41.52-52.452h41.98L1160.1 70.34l52.79 66.696h-41.98zM178.73 193.052V3.642h15.717l18.182 23.312 1.581-3.627c2.697-6.2 8.711-11.516 17.871-15.81 8.727-4.092 25.327-6.634 43.322-6.634 45.369 0 69.363 23.855 69.363 69.006 0 69.89-52.204 69.89-69.363 69.89-17.995 0-34.595-2.542-43.322-6.634-4.185-1.953-7.75-4.154-10.602-6.526l-3.859-3.208v69.641h-38.905.015zm85.839-163.649c-21.467 0-40.687 9.424-46.732 22.94l-.186.419v.45c-.14 2.96-.248 14.12-.233 17.111 0 3.085.093 14.54.217 17.515l.031.636.341.542c9.486 14.756 25.591 23.235 44.19 23.235 9.285 0 18.693-2.139 27.203-6.185 14.477-7.486 17.484-23.575 17.453-35.758.031-12.168-2.961-28.257-17.376-35.712-7.161-3.41-15.748-5.193-24.893-5.193h-.015zM417.833 139.779c-22.351 0-59.83-5.658-59.83-43.524 0-34.767 39.587-42.067 72.788-42.067h40.579V51.01c0-17.81-14.88-21.467-37.231-21.467-15.95 0-51.491 3.906-61.535 5.037V5.704C380.772 4.479 412.547 0 437.859 0c25.311 0 42.346 4.805 54.436 14.306 12.089 9.487 17.964 22.243 17.964 38.998v83.731h-15.717l-18.181-23.312-1.581 3.627c-2.697 6.2-8.711 11.517-17.872 15.81-9.393 4.402-22.552 6.634-39.091 6.634l.016-.015zm12.136-63.147c-8.168 0-33.046 0-33.046 18.243 0 8.37 5.007 18.352 28.815 18.352 12.4 0 22.754-2.666 30.752-7.936 7.92-5.208 14.88-17.5 14.88-26.319v-2.356h-41.401v.016zM654.114 137.035L612.59 84.583h-36.952v52.452h-38.905V3.642h38.905v52.452h36.952l41.524-52.452h41.974L643.295 70.34l52.793 66.696h-41.974zM760.041 136.911V32.101h-57.334l8.199-28.459h144.072V32.1h-56.032v104.811h-38.905zM984.992 3.751l-71.408 90.504V3.642h-38.781v133.269h42.904l70.602-89.45v89.45h38.781V3.642l-42.098.109z" fill="#2C3133"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -118,7 +118,7 @@ export function Topbar({ active }: TopbarProps) {
{brand ? (
/* logo_url из бренд-конфига safeUrl (блок javascript:/data: схем, #766).
Брендовый логотип полноразмерный wordmark (содержит название), поэтому
отдельный brand-product текст НЕ рендерим, иначе дубль («ПРАКТИКА» ×2).
отдельный brand-product текст НЕ рендерим, иначе дубль названия бренда ×2.
Невалидный/пустой URL fallback TI-марка + текстовое название бренда. */
safeUrl(brand.logo_url) ? (
<img

View file

@ -1,6 +1,6 @@
/* #657 white-label: Manrope OPEN geometric grotesk, визуальная замена
платного HalvarMittel из брендбука «Практики». Подключается всегда, но
применяется только при активном бренде (см. --font-sans override ниже). */
/* #657 white-label: Manrope OPEN geometric grotesk для брендового режима.
Подключается всегда, но применяется только при активном бренде
(см. --font-sans override ниже). */
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');
:root {

View file

@ -1,12 +1,11 @@
"use client";
/**
* useBrand white-label resolver для #657 («Практика» и пр.).
* useBrand white-label resolver (#657).
*
* Активный бренд резолвится из ДВУХ источников (account-бренд приоритетен, #765):
* 1. `me.brand` из /me привязка бренда к аккаунту (#657 brand-by-account) WINS;
* 2. query-параметр `?brand=<slug>` демо/превью, только если у аккаунта нет бренда.
* (praktika/kopylov логинятся свой бренд автоматически из me.brand, без ?brand=.)
* Если ни один не задан нет бренда (generic UI), хук возвращает {data: null}.
*
* Mirror паттерна useMe.ts: TanStack Query, staleTime/gcTime Infinity