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

@@ -0,0 +1,43 @@
# alembic/versions/x2c3d4e5f6g7_make_marketplace_product_id_nullable.py
"""Make marketplace_product_id nullable for direct product creation.
Revision ID: x2c3d4e5f6g7
Revises: w1b2c3d4e5f6
Create Date: 2026-01-06 23:15:00.000000
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "x2c3d4e5f6g7"
down_revision: str = "w1b2c3d4e5f6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Make marketplace_product_id nullable to allow direct product creation."""
# SQLite doesn't support ALTER COLUMN, so we need to recreate the table
# For SQLite, we use batch mode which handles this automatically
with op.batch_alter_table("products") as batch_op:
batch_op.alter_column(
"marketplace_product_id",
existing_type=sa.Integer(),
nullable=True,
)
def downgrade() -> None:
"""Revert marketplace_product_id to NOT NULL."""
# Note: This will fail if there are any NULL values in the column
with op.batch_alter_table("products") as batch_op:
batch_op.alter_column(
"marketplace_product_id",
existing_type=sa.Integer(),
nullable=False,
)

View File

@@ -0,0 +1,47 @@
# alembic/versions/y3d4e5f6g7h8_add_product_type_columns.py
"""Add is_digital and product_type columns to products table.
Makes Product fully independent from MarketplaceProduct for product type info.
Revision ID: y3d4e5f6g7h8
Revises: x2c3d4e5f6g7
Create Date: 2026-01-07 10:00:00.000000
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "y3d4e5f6g7h8"
down_revision: str = "x2c3d4e5f6g7"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Add is_digital and product_type columns to products table."""
with op.batch_alter_table("products") as batch_op:
batch_op.add_column(
sa.Column("is_digital", sa.Boolean(), nullable=False, server_default="0")
)
batch_op.add_column(
sa.Column(
"product_type",
sa.String(20),
nullable=False,
server_default="physical",
)
)
batch_op.create_index("idx_product_is_digital", ["is_digital"])
def downgrade() -> None:
"""Remove is_digital and product_type columns."""
with op.batch_alter_table("products") as batch_op:
batch_op.drop_index("idx_product_is_digital")
batch_op.drop_column("product_type")
batch_op.drop_column("is_digital")