feat: make Product fully independent from MarketplaceProduct
- Add is_digital and product_type columns to Product model - Remove is_digital/product_type properties that derived from MarketplaceProduct - Update Create form with translation tabs, GTIN type, sale price, VAT rate, image - Update Edit form to allow editing is_digital (remove disabled state) - Add Availability field to Edit form - Fix Detail page for directly created products (no marketplace source) - Update vendor_product_service to handle new fields in create/update - Add VendorProductCreate/Update schema fields for translations and is_digital - Add unit tests for is_digital column and direct product creation - Add integration tests for create/update API with new fields - Create product-architecture.md documenting the independent copy pattern - Add migration y3d4e5f6g7h8 for is_digital and product_type columns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -210,3 +210,287 @@ class TestAdminVendorProductsAPI:
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.products
|
||||
class TestAdminVendorProductCreateAPI:
|
||||
"""Tests for admin vendor product creation endpoints."""
|
||||
|
||||
def test_create_vendor_product_with_translations(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test creating a product with multi-language translations."""
|
||||
payload = {
|
||||
"vendor_id": test_vendor.id,
|
||||
"translations": {
|
||||
"en": {"title": "Test Product EN", "description": "English description"},
|
||||
"fr": {"title": "Test Product FR", "description": "French description"},
|
||||
},
|
||||
"vendor_sku": "CREATE_TEST_001",
|
||||
"brand": "TestBrand",
|
||||
"gtin": "1234567890123",
|
||||
"gtin_type": "ean13",
|
||||
"price": 29.99,
|
||||
"currency": "EUR",
|
||||
"tax_rate_percent": 17,
|
||||
"is_active": True,
|
||||
"is_digital": False,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/vendor-products",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["message"] == "Product created successfully"
|
||||
|
||||
# Verify the created product
|
||||
product_id = data["id"]
|
||||
detail_response = client.get(
|
||||
f"/api/v1/admin/vendor-products/{product_id}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert detail_response.status_code == 200
|
||||
detail = detail_response.json()
|
||||
assert detail["vendor_id"] == test_vendor.id
|
||||
assert detail["vendor_sku"] == "CREATE_TEST_001"
|
||||
assert detail["brand"] == "TestBrand"
|
||||
assert detail["is_digital"] is False
|
||||
assert detail["vendor_translations"]["en"]["title"] == "Test Product EN"
|
||||
|
||||
def test_create_digital_product(self, client, admin_headers, test_vendor):
|
||||
"""Test creating a digital product directly."""
|
||||
payload = {
|
||||
"vendor_id": test_vendor.id,
|
||||
"translations": {
|
||||
"en": {"title": "Digital Game Key", "description": "Steam game key"},
|
||||
},
|
||||
"vendor_sku": "DIGITAL_001",
|
||||
"price": 49.99,
|
||||
"is_digital": True,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/vendor-products",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Verify the product is digital
|
||||
detail_response = client.get(
|
||||
f"/api/v1/admin/vendor-products/{data['id']}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
detail = detail_response.json()
|
||||
assert detail["is_digital"] is True
|
||||
assert detail["product_type"] == "digital"
|
||||
|
||||
def test_create_product_without_marketplace_source(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test creating a direct product without marketplace source."""
|
||||
payload = {
|
||||
"vendor_id": test_vendor.id,
|
||||
"translations": {
|
||||
"en": {"title": "Direct Product", "description": "Created directly"},
|
||||
},
|
||||
"vendor_sku": "DIRECT_001",
|
||||
"brand": "DirectBrand",
|
||||
"price": 19.99,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/vendor-products",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Verify there's no marketplace source
|
||||
detail_response = client.get(
|
||||
f"/api/v1/admin/vendor-products/{data['id']}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
detail = detail_response.json()
|
||||
assert detail["marketplace_product_id"] is None
|
||||
assert detail["source_marketplace"] is None
|
||||
assert detail["source_vendor"] is None
|
||||
|
||||
def test_create_product_non_admin(self, client, auth_headers, test_vendor):
|
||||
"""Test non-admin trying to create product."""
|
||||
payload = {
|
||||
"vendor_id": test_vendor.id,
|
||||
"translations": {"en": {"title": "Test"}},
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/admin/vendor-products",
|
||||
json=payload,
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
@pytest.mark.products
|
||||
class TestAdminVendorProductUpdateAPI:
|
||||
"""Tests for admin vendor product update endpoints."""
|
||||
|
||||
def test_update_vendor_product_translations(
|
||||
self, client, admin_headers, test_vendor
|
||||
):
|
||||
"""Test updating product translations by first creating a product with translations."""
|
||||
# First create a product with translations
|
||||
create_payload = {
|
||||
"vendor_id": test_vendor.id,
|
||||
"translations": {
|
||||
"en": {"title": "Original Title EN", "description": "Original desc"},
|
||||
},
|
||||
"vendor_sku": "TRANS_TEST_001",
|
||||
"price": 10.00,
|
||||
"is_active": True,
|
||||
}
|
||||
create_response = client.post(
|
||||
"/api/v1/admin/vendor-products",
|
||||
json=create_payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
product_id = create_response.json()["id"]
|
||||
|
||||
# Now update the translations
|
||||
update_payload = {
|
||||
"translations": {
|
||||
"en": {"title": "Updated Title EN", "description": "Updated desc EN"},
|
||||
"de": {"title": "Updated Title DE", "description": "Updated desc DE"},
|
||||
}
|
||||
}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/vendor-products/{product_id}",
|
||||
json=update_payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Re-fetch the product to verify translations were saved
|
||||
detail_response = client.get(
|
||||
f"/api/v1/admin/vendor-products/{product_id}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert detail_response.status_code == 200
|
||||
data = detail_response.json()
|
||||
|
||||
# Check translations are present and updated
|
||||
assert "vendor_translations" in data
|
||||
assert data["vendor_translations"] is not None
|
||||
assert "en" in data["vendor_translations"]
|
||||
assert data["vendor_translations"]["en"]["title"] == "Updated Title EN"
|
||||
|
||||
def test_update_vendor_product_is_digital(
|
||||
self, client, admin_headers, test_product, db
|
||||
):
|
||||
"""Test updating product is_digital flag."""
|
||||
# First ensure it's not digital
|
||||
test_product.is_digital = False
|
||||
db.commit()
|
||||
|
||||
payload = {"is_digital": True}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/vendor-products/{test_product.id}",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_digital"] is True
|
||||
|
||||
def test_update_vendor_product_pricing(self, client, admin_headers, test_product):
|
||||
"""Test updating product pricing fields."""
|
||||
payload = {
|
||||
"price": 99.99,
|
||||
"sale_price": 79.99,
|
||||
"tax_rate_percent": 8,
|
||||
"availability": "in_stock",
|
||||
}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/vendor-products/{test_product.id}",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["price"] == 99.99
|
||||
assert data["sale_price"] == 79.99
|
||||
assert data["tax_rate_percent"] == 8
|
||||
assert data["availability"] == "in_stock"
|
||||
|
||||
def test_update_vendor_product_identifiers(
|
||||
self, client, admin_headers, test_product
|
||||
):
|
||||
"""Test updating product identifiers."""
|
||||
payload = {
|
||||
"vendor_sku": "UPDATED_SKU_001",
|
||||
"brand": "UpdatedBrand",
|
||||
"gtin": "9876543210123",
|
||||
"gtin_type": "ean13",
|
||||
}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/vendor-products/{test_product.id}",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["vendor_sku"] == "UPDATED_SKU_001"
|
||||
assert data["brand"] == "UpdatedBrand"
|
||||
assert data["gtin"] == "9876543210123"
|
||||
|
||||
def test_update_vendor_product_not_found(self, client, admin_headers):
|
||||
"""Test updating non-existent product."""
|
||||
payload = {"brand": "Test"}
|
||||
|
||||
response = client.patch(
|
||||
"/api/v1/admin/vendor-products/99999",
|
||||
json=payload,
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["error_code"] == "PRODUCT_NOT_FOUND"
|
||||
|
||||
def test_update_vendor_product_non_admin(self, client, auth_headers, test_product):
|
||||
"""Test non-admin trying to update product."""
|
||||
payload = {"brand": "Test"}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/vendor-products/{test_product.id}",
|
||||
json=payload,
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user