feat: add Letzshop CSV product export for admin and vendor

- Create letzshop_export_service.py with CSV generation logic
- Add admin endpoint: GET /api/v1/admin/vendors/{vendor}/export/letzshop
- Add vendor endpoint: GET /api/v1/vendor/letzshop/export
- Support language selection (en, fr, de) and include_inactive filter
- Generate Google Shopping compatible tab-delimited CSV format
- Add comprehensive integration tests for both endpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 18:04:04 +01:00
parent 0f99130b3d
commit d21cd366dc
5 changed files with 812 additions and 0 deletions

View File

@@ -359,3 +359,197 @@ class TestAdminLetzshopAccessControl:
response = client.get("/api/v1/admin/letzshop/vendors")
assert response.status_code == 401
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.admin
@pytest.mark.letzshop
class TestAdminLetzshopExportAPI:
"""Test admin Letzshop product export endpoints."""
def test_export_vendor_products_empty(
self, client, admin_headers, test_vendor
):
"""Test exporting products when vendor has no products."""
response = client.get(
f"/api/v1/admin/vendors/{test_vendor.id}/export/letzshop",
headers=admin_headers,
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
# Should have header row at minimum
content = response.text
assert "id\ttitle\tdescription" in content
def test_export_vendor_products_with_data(
self, client, db, admin_headers, test_vendor
):
"""Test exporting products with actual data."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
# Create marketplace product
mp = MarketplaceProduct(
marketplace_product_id="EXPORT-TEST-001",
price="29.99",
price_numeric=29.99,
currency="EUR",
brand="TestBrand",
availability="in stock",
is_active=True,
)
db.add(mp)
db.flush()
# Add translation
translation = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="en",
title="Export Test Product",
description="A product for testing exports",
)
db.add(translation)
db.flush()
# Create product linked to vendor
product = Product(
vendor_id=test_vendor.id,
vendor_sku="EXP-001",
marketplace_product_id=mp.id,
is_active=True,
)
db.add(product)
db.commit()
response = client.get(
f"/api/v1/admin/vendors/{test_vendor.id}/export/letzshop",
headers=admin_headers,
)
assert response.status_code == 200
content = response.text
assert "EXP-001" in content
assert "Export Test Product" in content
assert "29.99 EUR" in content
def test_export_vendor_products_french(
self, client, db, admin_headers, test_vendor
):
"""Test exporting products in French."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
mp = MarketplaceProduct(
marketplace_product_id="EXPORT-FR-001",
price="19.99",
is_active=True,
)
db.add(mp)
db.flush()
# Add French translation
translation_fr = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="fr",
title="Produit Test Export",
description="Un produit pour tester les exportations",
)
db.add(translation_fr)
db.flush()
product = Product(
vendor_id=test_vendor.id,
vendor_sku="EXP-FR-001",
marketplace_product_id=mp.id,
is_active=True,
)
db.add(product)
db.commit()
response = client.get(
f"/api/v1/admin/vendors/{test_vendor.id}/export/letzshop?language=fr",
headers=admin_headers,
)
assert response.status_code == 200
content = response.text
assert "Produit Test Export" in content
def test_export_vendor_products_include_inactive(
self, client, db, admin_headers, test_vendor
):
"""Test exporting including inactive products."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
# Create inactive product
mp = MarketplaceProduct(
marketplace_product_id="EXPORT-INACTIVE-001",
price="5.99",
is_active=True,
)
db.add(mp)
db.flush()
translation = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="en",
title="Inactive Product",
)
db.add(translation)
db.flush()
product = Product(
vendor_id=test_vendor.id,
vendor_sku="INACTIVE-001",
marketplace_product_id=mp.id,
is_active=False, # Inactive
)
db.add(product)
db.commit()
# Without include_inactive
response = client.get(
f"/api/v1/admin/vendors/{test_vendor.id}/export/letzshop",
headers=admin_headers,
)
assert "INACTIVE-001" not in response.text
# With include_inactive
response = client.get(
f"/api/v1/admin/vendors/{test_vendor.id}/export/letzshop?include_inactive=true",
headers=admin_headers,
)
assert "INACTIVE-001" in response.text
def test_export_vendor_products_by_vendor_code(
self, client, admin_headers, test_vendor
):
"""Test exporting products using vendor_code identifier."""
response = client.get(
f"/api/v1/admin/vendors/{test_vendor.vendor_code}/export/letzshop",
headers=admin_headers,
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
def test_export_vendor_not_found(self, client, admin_headers):
"""Test exporting for non-existent vendor."""
response = client.get(
"/api/v1/admin/vendors/999999/export/letzshop",
headers=admin_headers,
)
assert response.status_code == 404

View File

@@ -497,3 +497,234 @@ class TestVendorLetzshopFulfillmentAPI:
data = response.json()
assert data["success"] is True
assert data["tracking_number"] == "1Z999AA1"
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.vendor
@pytest.mark.letzshop
class TestVendorLetzshopExportAPI:
"""Test vendor Letzshop product export endpoints."""
def test_export_products_empty(
self, client, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test exporting products when vendor has no products."""
response = client.get(
"/api/v1/vendor/letzshop/export", headers=vendor_user_headers
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
# Should have header row at minimum
content = response.text
assert "id\ttitle\tdescription" in content
def test_export_products_with_data(
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test exporting products with actual data."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
# Create marketplace product
mp = MarketplaceProduct(
marketplace_product_id="VENDOR-EXPORT-001",
price="49.99",
price_numeric=49.99,
currency="EUR",
brand="VendorBrand",
availability="in stock",
is_active=True,
)
db.add(mp)
db.flush()
# Add translation
translation = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="en",
title="Vendor Export Product",
description="A product for testing vendor exports",
)
db.add(translation)
db.flush()
# Create product linked to vendor
product = Product(
vendor_id=test_vendor_with_vendor_user.id,
vendor_sku="VEXP-001",
marketplace_product_id=mp.id,
is_active=True,
)
db.add(product)
db.commit()
response = client.get(
"/api/v1/vendor/letzshop/export", headers=vendor_user_headers
)
assert response.status_code == 200
content = response.text
assert "VEXP-001" in content
assert "Vendor Export Product" in content
assert "49.99 EUR" in content
def test_export_products_french_language(
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test exporting products in French."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
mp = MarketplaceProduct(
marketplace_product_id="VENDOR-FR-001",
price="39.99",
is_active=True,
)
db.add(mp)
db.flush()
# Add French translation
translation_fr = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="fr",
title="Produit Vendeur Export",
description="Un produit pour tester",
)
db.add(translation_fr)
db.flush()
product = Product(
vendor_id=test_vendor_with_vendor_user.id,
vendor_sku="VFR-001",
marketplace_product_id=mp.id,
is_active=True,
)
db.add(product)
db.commit()
response = client.get(
"/api/v1/vendor/letzshop/export?language=fr",
headers=vendor_user_headers,
)
assert response.status_code == 200
content = response.text
assert "Produit Vendeur Export" in content
def test_export_products_german_language(
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test exporting products in German."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
mp = MarketplaceProduct(
marketplace_product_id="VENDOR-DE-001",
price="59.99",
is_active=True,
)
db.add(mp)
db.flush()
translation_de = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="de",
title="Deutsches Exportprodukt",
)
db.add(translation_de)
db.flush()
product = Product(
vendor_id=test_vendor_with_vendor_user.id,
vendor_sku="VDE-001",
marketplace_product_id=mp.id,
is_active=True,
)
db.add(product)
db.commit()
response = client.get(
"/api/v1/vendor/letzshop/export?language=de",
headers=vendor_user_headers,
)
assert response.status_code == 200
content = response.text
assert "Deutsches Exportprodukt" in content
def test_export_products_include_inactive(
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test exporting including inactive products."""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
MarketplaceProductTranslation,
)
mp = MarketplaceProduct(
marketplace_product_id="VENDOR-INACTIVE-001",
price="9.99",
is_active=True,
)
db.add(mp)
db.flush()
translation = MarketplaceProductTranslation(
marketplace_product_id=mp.id,
language="en",
title="Inactive Vendor Product",
)
db.add(translation)
db.flush()
product = Product(
vendor_id=test_vendor_with_vendor_user.id,
vendor_sku="VINACTIVE-001",
marketplace_product_id=mp.id,
is_active=False, # Inactive
)
db.add(product)
db.commit()
# Without include_inactive
response = client.get(
"/api/v1/vendor/letzshop/export",
headers=vendor_user_headers,
)
assert "VINACTIVE-001" not in response.text
# With include_inactive
response = client.get(
"/api/v1/vendor/letzshop/export?include_inactive=true",
headers=vendor_user_headers,
)
assert "VINACTIVE-001" in response.text
def test_export_unauthenticated(self, client):
"""Test that unauthenticated requests are rejected."""
response = client.get("/api/v1/vendor/letzshop/export")
assert response.status_code == 401
def test_export_filename_uses_vendor_code(
self, client, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test that export filename uses vendor code."""
response = client.get(
"/api/v1/vendor/letzshop/export", headers=vendor_user_headers
)
assert response.status_code == 200
content_disposition = response.headers.get("content-disposition", "")
assert "letzshop_export.csv" in content_disposition