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:
2026-01-08 01:11:00 +01:00
parent 7b81f59eba
commit fa2a3bf89a
19 changed files with 1603 additions and 201 deletions

View File

@@ -21,17 +21,33 @@
<!-- Product Details -->
<div x-show="!loading && product">
<!-- Override Info Banner -->
<div class="px-4 py-3 mb-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg shadow-md">
<!-- Info Banner - adapts based on whether product has marketplace source -->
<div class="px-4 py-3 mb-6 rounded-lg shadow-md"
:class="product?.marketplace_product_id ? 'bg-purple-50 dark:bg-purple-900/20' : 'bg-blue-50 dark:bg-blue-900/20'">
<div class="flex items-start">
<span x-html="$icon('information-circle', 'w-5 h-5 text-purple-500 mr-3 mt-0.5 flex-shrink-0')"></span>
<span x-html="$icon('information-circle', product?.marketplace_product_id ? 'w-5 h-5 text-purple-500 mr-3 mt-0.5 flex-shrink-0' : 'w-5 h-5 text-blue-500 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Vendor Product Catalog Entry</p>
<p class="text-xs text-purple-600 dark:text-purple-400 mt-1">
This is a vendor-specific copy of a marketplace product. Fields marked with
<span class="inline-flex items-center px-1.5 py-0.5 mx-1 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
have been customized for this vendor.
</p>
<!-- Marketplace-sourced product -->
<template x-if="product?.marketplace_product_id">
<div>
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Vendor Product Catalog Entry</p>
<p class="text-xs text-purple-600 dark:text-purple-400 mt-1">
This is a vendor-specific copy of a marketplace product. Fields marked with
<span class="inline-flex items-center px-1.5 py-0.5 mx-1 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
have been customized for this vendor.
</p>
</div>
</template>
<!-- Directly created product -->
<template x-if="!product?.marketplace_product_id">
<div>
<p class="text-sm font-medium text-blue-700 dark:text-blue-300">Directly Created Product</p>
<p class="text-xs text-blue-600 dark:text-blue-400 mt-1">
This product was created directly for this vendor without a marketplace source.
All product information is managed independently.
</p>
</div>
</template>
</div>
</div>
</div>
@@ -42,18 +58,20 @@
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<!-- View Source Product - only for marketplace-sourced products -->
<a
x-show="product?.marketplace_product_id"
:href="'/admin/marketplace-products/' + product?.marketplace_product_id"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
<span x-html="$icon('database', 'w-4 h-4 mr-2')"></span>
View Source Product
</a>
<button
@click="openEditModal()"
<a
:href="'/admin/vendor-products/' + productId + '/edit'"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600">
<span x-html="$icon('pencil', 'w-4 h-4 mr-2')"></span>
Edit Overrides
</button>
<span x-text="product?.marketplace_product_id ? 'Edit Overrides' : 'Edit Product'"></span>
</a>
<button
@click="toggleActive()"
:class="product?.is_active
@@ -194,7 +212,7 @@
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Product Identifiers
</h3>
<div class="grid gap-4 md:grid-cols-4">
<div class="grid gap-4" :class="product?.marketplace_product_id ? 'md:grid-cols-4' : 'md:grid-cols-3'">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Product ID</p>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.id || '-'">-</p>
@@ -207,41 +225,62 @@
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Vendor SKU</p>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.vendor_sku || '-'">-</p>
</div>
<div>
<!-- Source SKU - only for marketplace-sourced products -->
<div x-show="product?.marketplace_product_id">
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source SKU</p>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.source_sku || '-'">-</p>
</div>
</div>
</div>
<!-- Source Information Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Source Information
<!-- Source Information Card - only for marketplace-sourced products -->
<template x-if="product?.marketplace_product_id">
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Source Information
</h3>
<a
:href="'/admin/marketplace-products/' + product?.marketplace_product_id"
class="flex items-center text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400">
<span>View Source</span>
<span x-html="$icon('arrow-right', 'w-4 h-4 ml-1')"></span>
</a>
</div>
<div class="grid gap-4 md:grid-cols-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Marketplace</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.source_marketplace || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source Vendor</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.source_vendor || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Marketplace Product ID</p>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.marketplace_product_id || '-'">-</p>
</div>
</div>
</div>
</template>
<!-- Product Origin Card - for directly created products -->
<template x-if="!product?.marketplace_product_id">
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Product Origin
</h3>
<a
:href="'/admin/marketplace-products/' + product?.marketplace_product_id"
class="flex items-center text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400">
<span>View Source</span>
<span x-html="$icon('arrow-right', 'w-4 h-4 ml-1')"></span>
</a>
</div>
<div class="grid gap-4 md:grid-cols-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Marketplace</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.source_marketplace || 'Unknown'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source Vendor</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.source_vendor || 'Unknown'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Marketplace Product ID</p>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.marketplace_product_id || '-'">-</p>
<div class="flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30">
<span x-html="$icon('plus-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400')"></span>
</span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Direct Creation</p>
<p class="text-xs text-gray-500 dark:text-gray-400">This product was created directly in the vendor's catalog</p>
</div>
</div>
</div>
</div>
</template>
<!-- Description Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="product?.title || product?.description">