refactor: remove all backward compatibility code across 70 files
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running

Clean up 28 backward compatibility instances identified in the codebase.
The app is not live, so all shims are replaced with the target architecture:

- Remove legacy Inventory.location column (use bin_location exclusively)
- Remove dashboard _extract_metric_value helper (use flat metrics dict)
- Remove legacy stat field duplicates (total_stores, total_imports, etc.)
- Remove 13 re-export shims and class aliases across modules
- Remove module-enabling JSON fallback (use PlatformModule junction table)
- Remove menu_to_legacy_format() conversion (return dataclasses directly)
- Remove title/description from MarketplaceProductBase schema
- Clean billing convenience method docstrings
- Clean test fixtures and backward-compat comments
- Add PlatformModule seeding to init_production.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 13:20:29 +01:00
parent b0db8133a0
commit aad18c27ab
70 changed files with 501 additions and 841 deletions

View File

@@ -12,8 +12,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.modules.analytics.schemas import ImportStatsResponse # IMPORT-002
from app.modules.analytics.services.stats_service import stats_service # IMPORT-002
from app.modules.core.schemas.dashboard import ImportStatsResponse
from app.modules.enums import FrontendType
from app.modules.marketplace.schemas import (
AdminMarketplaceImportJobListResponse,

View File

@@ -1,9 +1,10 @@
# app/modules/marketplace/schemas/marketplace_product.py
"""Pydantic schemas for MarketplaceProduct API validation.
Note: title and description are stored in MarketplaceProductTranslation table,
but we keep them in the API schemas for convenience. The service layer
handles creating/updating translations separately.
Note: title and description are stored in MarketplaceProductTranslation table.
The service layer handles creating/updating translations separately via
dedicated title/description parameters. MarketplaceProductCreate includes
a required title field for API convenience.
"""
from datetime import datetime
@@ -32,10 +33,6 @@ class MarketplaceProductBase(BaseModel):
marketplace_product_id: str | None = None
# Localized fields (passed to translations)
title: str | None = None
description: str | None = None
# Links and media
link: str | None = None
image_link: str | None = None

View File

@@ -112,10 +112,7 @@ class MarketplaceProductService:
)
# Create the product (without title/description - those go in translations)
product_dict = product_data.model_dump()
# Remove any title/description if present in schema (for backwards compatibility)
product_dict.pop("title", None)
product_dict.pop("description", None)
product_dict = product_data.model_dump(exclude={"title", "description"})
db_product = MarketplaceProduct(**product_dict)
db.add(db_product)
@@ -259,17 +256,21 @@ class MarketplaceProductService:
MarketplaceProduct.store_name.ilike(search_term),
MarketplaceProduct.brand.ilike(search_term),
MarketplaceProduct.gtin.ilike(search_term),
MarketplaceProduct.marketplace_product_id.ilike(search_term),
MarketplaceProduct.marketplace_product_id.ilike(
search_term
),
MarketplaceProductTranslation.title.ilike(search_term),
MarketplaceProductTranslation.description.ilike(search_term),
MarketplaceProductTranslation.description.ilike(
search_term
),
)
)
.distinct()
.subquery()
)
query = query.filter(MarketplaceProduct.id.in_(
db.query(id_subquery.c.id)
))
query = query.filter(
MarketplaceProduct.id.in_(db.query(id_subquery.c.id))
)
total = query.count()
products = query.offset(skip).limit(limit).all()
@@ -305,12 +306,10 @@ class MarketplaceProductService:
try:
product = self.get_product_by_id_or_raise(db, marketplace_product_id)
# Update fields
update_data = product_update.model_dump(exclude_unset=True)
# Remove title/description from update data (handled separately)
update_data.pop("title", None)
update_data.pop("description", None)
# Update fields (exclude title/description - handled separately via translations)
update_data = product_update.model_dump(
exclude_unset=True, exclude={"title", "description"}
)
# Validate GTIN if being updated
if "gtin" in update_data and update_data["gtin"]:
@@ -447,14 +446,16 @@ class MarketplaceProductService:
"""
try:
# SVC-005 - Admin/internal function for inventory lookup by GTIN
inventory_entries = db.query(Inventory).filter(Inventory.gtin == gtin).all() # SVC-005
inventory_entries = (
db.query(Inventory).filter(Inventory.gtin == gtin).all()
) # SVC-005
if not inventory_entries:
return None
total_quantity = sum(entry.quantity for entry in inventory_entries)
locations = [
InventoryLocationResponse(
location=entry.location,
location=entry.bin_location,
quantity=entry.quantity,
reserved_quantity=entry.reserved_quantity or 0,
available_quantity=entry.quantity - (entry.reserved_quantity or 0),
@@ -661,17 +662,13 @@ class MarketplaceProductService:
.distinct()
.subquery()
)
query = query.filter(MarketplaceProduct.id.in_(
db.query(id_subquery.c.id)
))
query = query.filter(MarketplaceProduct.id.in_(db.query(id_subquery.c.id)))
if marketplace:
query = query.filter(MarketplaceProduct.marketplace == marketplace)
if store_name:
query = query.filter(
MarketplaceProduct.store_name.ilike(f"%{store_name}%")
)
query = query.filter(MarketplaceProduct.store_name.ilike(f"%{store_name}%"))
if availability:
query = query.filter(MarketplaceProduct.availability == availability)
@@ -966,8 +963,12 @@ class MarketplaceProductService:
primary_image_url=mp.image_link,
additional_images=mp.additional_images,
# === Digital product fields ===
download_url=mp.download_url if hasattr(mp, "download_url") else None,
license_type=mp.license_type if hasattr(mp, "license_type") else None,
download_url=mp.download_url
if hasattr(mp, "download_url")
else None,
license_type=mp.license_type
if hasattr(mp, "license_type")
else None,
)
db.add(product)
@@ -990,12 +991,14 @@ class MarketplaceProductService:
translations_copied += 1
copied += 1
details.append({
"id": mp.id,
"status": "copied",
"gtin": mp.gtin,
"translations_copied": translations_copied,
})
details.append(
{
"id": mp.id,
"status": "copied",
"gtin": mp.gtin,
"translations_copied": translations_copied,
}
)
except SQLAlchemyError as e:
logger.error(f"Failed to copy product {mp.id}: {str(e)}")

View File

@@ -204,7 +204,7 @@ class TestProductService:
def test_update_product_not_found(self, db):
"""Test updating non-existent product raises MarketplaceProductNotFoundException"""
update_data = MarketplaceProductUpdate(title="Updated Title")
update_data = MarketplaceProductUpdate(brand="Updated Brand")
with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.update_product(db, "NONEXISTENT", update_data)
@@ -230,10 +230,13 @@ class TestProductService:
):
"""Test updating product with empty title preserves existing title in translation"""
original_title = test_marketplace_product.get_title()
update_data = MarketplaceProductUpdate(title="")
update_data = MarketplaceProductUpdate()
updated_product = self.service.update_product(
db, test_marketplace_product.marketplace_product_id, update_data
db,
test_marketplace_product.marketplace_product_id,
update_data,
title="",
)
# Empty title update preserves existing translation title