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:
@@ -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
|
||||
|
||||
231
tests/integration/api/v1/vendor/test_letzshop.py
vendored
231
tests/integration/api/v1/vendor/test_letzshop.py
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user