feat: landing page analytics goals and sticky pay button

- Add sticky_pay_button, analytics_view_enabled, analytics_view_goal,
  analytics_click_enabled, analytics_click_goal columns to LandingPage
- Add fields to CRUD updatable fields, admin create/update/detail
  schemas, create_landing() kwargs, _landing_to_detail() response
- Expose sticky_pay_button and analytics fields in public landing
  config response for frontend Yandex Metrika integration
- Add migration 0062 with idempotent column checks

Based on PR #2852 by @smediainfo — CI/CD workflow changes excluded
(hardcoded version strings would regress dynamic manifest reading)
This commit is contained in:
Fringg
2026-04-22 05:36:41 +03:00
parent dd177101f7
commit 3272b4bb05
5 changed files with 109 additions and 0 deletions
+25
View File
@@ -205,6 +205,11 @@ class LandingCreateRequest(BaseModel):
discount_ends_at: datetime | None = None
discount_badge_text: dict[str, str] | None = None
background_config: dict | None = None
sticky_pay_button: bool = False
analytics_view_enabled: bool = False
analytics_view_goal: str | None = Field(default=None, max_length=64)
analytics_click_enabled: bool = False
analytics_click_goal: str | None = Field(default=None, max_length=64)
@field_validator('background_config')
@classmethod
@@ -314,6 +319,11 @@ class LandingUpdateRequest(BaseModel):
discount_ends_at: datetime | None = None
discount_badge_text: dict[str, str] | None = None
background_config: dict | None = None
sticky_pay_button: bool | None = None
analytics_view_enabled: bool | None = None
analytics_view_goal: str | None = Field(default=None, max_length=64)
analytics_click_enabled: bool | None = None
analytics_click_goal: str | None = Field(default=None, max_length=64)
@field_validator('background_config')
@classmethod
@@ -461,6 +471,11 @@ class LandingDetailResponse(BaseModel):
discount_ends_at: datetime | None = None
discount_badge_text: dict[str, str] | None = None
background_config: dict | None = None
sticky_pay_button: bool = False
analytics_view_enabled: bool = False
analytics_view_goal: str | None = None
analytics_click_enabled: bool = False
analytics_click_goal: str | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
@@ -638,6 +653,11 @@ async def create_landing_page(
discount_ends_at=request.discount_ends_at,
discount_badge_text=request.discount_badge_text,
background_config=request.background_config,
sticky_pay_button=request.sticky_pay_button,
analytics_view_enabled=request.analytics_view_enabled,
analytics_view_goal=request.analytics_view_goal,
analytics_click_enabled=request.analytics_click_enabled,
analytics_click_goal=request.analytics_click_goal,
)
logger.info('Admin created landing page', admin_id=admin.id, slug=landing.slug, landing_id=landing.id)
@@ -1068,6 +1088,11 @@ def _landing_to_detail(landing: LandingPage) -> LandingDetailResponse:
discount_ends_at=landing.discount_ends_at,
discount_badge_text=landing.discount_badge_text,
background_config=landing.background_config,
sticky_pay_button=landing.sticky_pay_button,
analytics_view_enabled=landing.analytics_view_enabled,
analytics_view_goal=landing.analytics_view_goal,
analytics_click_enabled=landing.analytics_click_enabled,
analytics_click_goal=landing.analytics_click_goal,
created_at=landing.created_at,
updated_at=landing.updated_at,
)
+10
View File
@@ -99,6 +99,11 @@ class LandingConfigResponse(BaseModel):
meta_description: str | None = None
discount: LandingDiscountInfo | None = None # null if no active discount
background_config: dict | None = None
sticky_pay_button: bool = False
analytics_view_enabled: bool = False
analytics_view_goal: str | None = None
analytics_click_enabled: bool = False
analytics_click_goal: str | None = None
_EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
@@ -535,6 +540,11 @@ async def get_landing_config(
meta_description=resolve_locale_text(landing.meta_description, lang) or None,
discount=discount,
background_config=landing.background_config,
sticky_pay_button=landing.sticky_pay_button,
analytics_view_enabled=landing.analytics_view_enabled,
analytics_view_goal=landing.analytics_view_goal,
analytics_click_enabled=landing.analytics_click_enabled,
analytics_click_goal=landing.analytics_click_goal,
)
+5
View File
@@ -76,6 +76,11 @@ _LANDING_UPDATABLE_FIELDS = frozenset(
'discount_ends_at',
'discount_badge_text',
'background_config',
'sticky_pay_button',
'analytics_view_enabled',
'analytics_view_goal',
'analytics_click_enabled',
'analytics_click_goal',
}
)
+7
View File
@@ -3459,6 +3459,13 @@ class LandingPage(Base):
background_config = Column(
JSON, nullable=True
) # AnimationConfig: {enabled, type, settings, opacity, blur, reducedOnMobile}
# Sticky pay button on mobile (full-width fixed bottom)
sticky_pay_button = Column(Boolean, nullable=False, default=False, server_default=text('false'))
# Yandex Metrika landing-level conversion goals
analytics_view_enabled = Column(Boolean, nullable=False, default=False, server_default=text('false'))
analytics_view_goal = Column(String(64), nullable=True)
analytics_click_enabled = Column(Boolean, nullable=False, default=False, server_default=text('false'))
analytics_click_goal = Column(String(64), nullable=True)
created_at = Column(AwareDateTime(), server_default=func.now())
updated_at = Column(AwareDateTime(), server_default=func.now(), onupdate=func.now())
@@ -0,0 +1,62 @@
"""add landing_pages analytics columns + sticky_pay_button
Revision ID: 0062
Revises: 0061
Create Date: 2026-04-22
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = '0062'
down_revision: Union[str, None] = '0061'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
NEW_COLUMNS = (
(
'sticky_pay_button',
sa.Column('sticky_pay_button', sa.Boolean(), nullable=False, server_default=sa.text('false')),
),
(
'analytics_view_enabled',
sa.Column('analytics_view_enabled', sa.Boolean(), nullable=False, server_default=sa.text('false')),
),
('analytics_view_goal', sa.Column('analytics_view_goal', sa.String(64), nullable=True)),
(
'analytics_click_enabled',
sa.Column('analytics_click_enabled', sa.Boolean(), nullable=False, server_default=sa.text('false')),
),
('analytics_click_goal', sa.Column('analytics_click_goal', sa.String(64), nullable=True)),
)
def upgrade() -> None:
conn = op.get_bind()
for col_name, col_def in NEW_COLUMNS:
result = conn.execute(
sa.text(
'SELECT EXISTS (SELECT 1 FROM information_schema.columns '
"WHERE table_name = 'landing_pages' AND column_name = :col)"
),
{'col': col_name},
)
if not result.scalar():
op.add_column('landing_pages', col_def)
def downgrade() -> None:
conn = op.get_bind()
for col_name, _ in reversed(NEW_COLUMNS):
result = conn.execute(
sa.text(
'SELECT EXISTS (SELECT 1 FROM information_schema.columns '
"WHERE table_name = 'landing_pages' AND column_name = :col)"
),
{'col': col_name},
)
if result.scalar():
op.drop_column('landing_pages', col_name)