refactor: update schemas for company-vendor architecture

- Update VendorResponse to use company_id instead of owner fields
- Add VendorDetailResponse with company info (name, email, phone, website)
- Update VendorCreate to require company_id
- Add owner_email, owner_username, vendors list to CompanyDetailResponse
- Remove obsolete VendorTransferOwnership schemas

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 19:39:16 +01:00
parent 5ba4603ac9
commit df8731d7ae
2 changed files with 111 additions and 128 deletions

View File

@@ -5,6 +5,9 @@ Pydantic schemas for Company model.
These schemas are used for API request/response validation and serialization. These schemas are used for API request/response validation and serialization.
""" """
from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
@@ -106,16 +109,24 @@ class CompanyResponse(BaseModel):
class CompanyDetailResponse(CompanyResponse): class CompanyDetailResponse(CompanyResponse):
""" """
Detailed company response including vendor count. Detailed company response including vendor count and owner details.
Used for company detail pages and admin views. Used for company detail pages and admin views.
""" """
# Owner details (from related User)
owner_email: str | None = Field(None, description="Owner's email address")
owner_username: str | None = Field(None, description="Owner's username")
# Vendor statistics
vendor_count: int = Field(0, description="Number of vendors under this company") vendor_count: int = Field(0, description="Number of vendors under this company")
active_vendor_count: int = Field( active_vendor_count: int = Field(
0, description="Number of active vendors under this company" 0, description="Number of active vendors under this company"
) )
# Vendors list (optional, for detail view)
vendors: list | None = Field(None, description="List of vendors under this company")
class CompanyListResponse(BaseModel): class CompanyListResponse(BaseModel):
"""Schema for paginated company list.""" """Schema for paginated company list."""
@@ -153,3 +164,53 @@ class CompanySummary(BaseModel):
is_active: bool is_active: bool
is_verified: bool is_verified: bool
vendor_count: int = 0 vendor_count: int = 0
class CompanyTransferOwnership(BaseModel):
"""
Schema for transferring company ownership to another user.
This is a critical operation that requires:
- Confirmation flag
- Reason for audit trail (optional)
"""
new_owner_user_id: int = Field(
..., description="ID of the user who will become the new owner", gt=0
)
confirm_transfer: bool = Field(
..., description="Must be true to confirm ownership transfer"
)
transfer_reason: str | None = Field(
None,
max_length=500,
description="Reason for ownership transfer (for audit logs)",
)
@field_validator("confirm_transfer")
@classmethod
def validate_confirmation(cls, v):
"""Ensure confirmation is explicitly true."""
if not v:
raise ValueError("Ownership transfer requires explicit confirmation")
return v
class CompanyTransferOwnershipResponse(BaseModel):
"""Response after successful ownership transfer."""
message: str
company_id: int
company_name: str
old_owner: dict[str, Any] = Field(
..., description="Information about the previous owner"
)
new_owner: dict[str, Any] = Field(
..., description="Information about the new owner"
)
transferred_at: datetime
transfer_reason: str | None

View File

@@ -3,25 +3,33 @@
Pydantic schemas for Vendor-related operations. Pydantic schemas for Vendor-related operations.
Schemas include: Schemas include:
- VendorCreate: For creating vendors with owner accounts - VendorCreate: For creating vendors under companies
- VendorUpdate: For updating vendor information (Admin only) - VendorUpdate: For updating vendor information (Admin only)
- VendorResponse: Standard vendor response - VendorResponse: Standard vendor response
- VendorDetailResponse: Vendor response with owner details - VendorDetailResponse: Vendor response with company/owner details
- VendorCreateResponse: Response after vendor creation (includes credentials) - VendorCreateResponse: Response after vendor creation
- VendorListResponse: Paginated vendor list - VendorListResponse: Paginated vendor list
- VendorSummary: Lightweight vendor info - VendorSummary: Lightweight vendor info
- VendorTransferOwnership: For transferring vendor ownership
Note: Ownership transfer is handled at the Company level.
See models/schema/company.py for CompanyTransferOwnership.
""" """
import re import re
from datetime import datetime from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator
class VendorCreate(BaseModel): class VendorCreate(BaseModel):
"""Schema for creating a new vendor with owner account.""" """
Schema for creating a new vendor (storefront/brand) under an existing company.
Business contact info is inherited from the parent company.
"""
# Parent company
company_id: int = Field(..., description="ID of the parent company", gt=0)
# Basic Information # Basic Information
vendor_code: str = Field( vendor_code: str = Field(
@@ -34,41 +42,15 @@ class VendorCreate(BaseModel):
..., description="Unique subdomain for the vendor", min_length=2, max_length=100 ..., description="Unique subdomain for the vendor", min_length=2, max_length=100
) )
name: str = Field( name: str = Field(
..., description="Display name of the vendor", min_length=2, max_length=255 ..., description="Display name of the vendor/brand", min_length=2, max_length=255
) )
description: str | None = Field(None, description="Vendor description") description: str | None = Field(None, description="Vendor/brand description")
# Owner Information (Creates User Account) # Marketplace URLs (brand-specific multi-language support)
owner_email: str = Field(
...,
description="Email for the vendor owner (used for login and authentication)",
)
# Business Contact Information (Vendor Fields)
contact_email: str | None = Field(
None,
description="Public business contact email (defaults to owner_email if not provided)",
)
contact_phone: str | None = Field(None, description="Contact phone number")
website: str | None = Field(None, description="Website URL")
# Business Details
business_address: str | None = Field(None, description="Business address")
tax_number: str | None = Field(None, description="Tax/VAT number")
# Marketplace URLs (multi-language support)
letzshop_csv_url_fr: str | None = Field(None, description="French CSV URL") letzshop_csv_url_fr: str | None = Field(None, description="French CSV URL")
letzshop_csv_url_en: str | None = Field(None, description="English CSV URL") letzshop_csv_url_en: str | None = Field(None, description="English CSV URL")
letzshop_csv_url_de: str | None = Field(None, description="German CSV URL") letzshop_csv_url_de: str | None = Field(None, description="German CSV URL")
@field_validator("owner_email", "contact_email")
@classmethod
def validate_emails(cls, v):
"""Validate email format and normalize to lowercase."""
if v and ("@" not in v or "." not in v):
raise ValueError("Invalid email format")
return v.lower() if v else v
@field_validator("subdomain") @field_validator("subdomain")
@classmethod @classmethod
def validate_subdomain(cls, v): def validate_subdomain(cls, v):
@@ -90,8 +72,8 @@ class VendorUpdate(BaseModel):
""" """
Schema for updating vendor information (Admin only). Schema for updating vendor information (Admin only).
Note: owner_email is NOT included here. To change the owner, Note: Business contact info (contact_email, etc.) is at the Company level.
use the transfer-ownership endpoint instead. Use company update endpoints to modify those fields.
""" """
# Basic Information # Basic Information
@@ -99,16 +81,7 @@ class VendorUpdate(BaseModel):
description: str | None = None description: str | None = None
subdomain: str | None = Field(None, min_length=2, max_length=100) subdomain: str | None = Field(None, min_length=2, max_length=100)
# Business Contact Information (Vendor Fields) # Marketplace URLs (brand-specific)
contact_email: str | None = Field(None, description="Public business contact email")
contact_phone: str | None = None
website: str | None = None
# Business Details
business_address: str | None = None
tax_number: str | None = None
# Marketplace URLs
letzshop_csv_url_fr: str | None = None letzshop_csv_url_fr: str | None = None
letzshop_csv_url_en: str | None = None letzshop_csv_url_en: str | None = None
letzshop_csv_url_de: str | None = None letzshop_csv_url_de: str | None = None
@@ -123,19 +96,17 @@ class VendorUpdate(BaseModel):
"""Normalize subdomain to lowercase.""" """Normalize subdomain to lowercase."""
return v.lower().strip() if v else v return v.lower().strip() if v else v
@field_validator("contact_email")
@classmethod
def validate_contact_email(cls, v):
"""Validate contact email format."""
if v and ("@" not in v or "." not in v):
raise ValueError("Invalid email format")
return v.lower() if v else v
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
class VendorResponse(BaseModel): class VendorResponse(BaseModel):
"""Standard schema for vendor response data.""" """
Standard schema for vendor response data.
Note: Business contact info (contact_email, contact_phone, website,
business_address, tax_number) is now at the Company level.
Use company_id to look up company details.
"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -144,18 +115,11 @@ class VendorResponse(BaseModel):
subdomain: str subdomain: str
name: str name: str
description: str | None description: str | None
owner_user_id: int
# Contact Information (Business) # Company relationship
contact_email: str | None company_id: int
contact_phone: str | None
website: str | None
# Business Information # Marketplace URLs (brand-specific)
business_address: str | None
tax_number: str | None
# Marketplace URLs
letzshop_csv_url_fr: str | None letzshop_csv_url_fr: str | None
letzshop_csv_url_en: str | None letzshop_csv_url_en: str | None
letzshop_csv_url_de: str | None letzshop_csv_url_de: str | None
@@ -171,30 +135,33 @@ class VendorResponse(BaseModel):
class VendorDetailResponse(VendorResponse): class VendorDetailResponse(VendorResponse):
""" """
Extended vendor response including owner information. Extended vendor response including company information.
Includes both: Includes company details like contact info and owner information.
- contact_email (business contact)
- owner_email (owner's authentication email)
""" """
# Company info
company_name: str = Field(..., description="Name of the parent company")
company_contact_email: str = Field(..., description="Company business contact email")
company_contact_phone: str | None = Field(None, description="Company phone number")
company_website: str | None = Field(None, description="Company website URL")
# Owner info (at company level)
owner_email: str = Field( owner_email: str = Field(
..., description="Email of the vendor owner (for login/authentication)" ..., description="Email of the company owner (for login/authentication)"
) )
owner_username: str = Field(..., description="Username of the vendor owner") owner_username: str = Field(..., description="Username of the company owner")
class VendorCreateResponse(VendorDetailResponse): class VendorCreateResponse(VendorDetailResponse):
""" """
Response after creating vendor - includes generated credentials. Response after creating vendor under an existing company.
IMPORTANT: temporary_password is shown only once! The vendor is created under a company, so no new owner credentials are generated.
The company owner already has access to this vendor.
""" """
temporary_password: str = Field( login_url: str | None = Field(None, description="URL for vendor storefront")
..., description="Temporary password for owner (SHOWN ONLY ONCE)"
)
login_url: str | None = Field(None, description="URL for vendor owner to login")
class VendorListResponse(BaseModel): class VendorListResponse(BaseModel):
@@ -215,55 +182,10 @@ class VendorSummary(BaseModel):
vendor_code: str vendor_code: str
subdomain: str subdomain: str
name: str name: str
company_id: int
is_active: bool is_active: bool
class VendorTransferOwnership(BaseModel): # NOTE: Vendor ownership transfer schemas have been removed.
""" # Ownership transfer is now handled at the Company level.
Schema for transferring vendor ownership to another user. # See models/schema/company.py for CompanyTransferOwnership and CompanyTransferOwnershipResponse.
This is a critical operation that requires:
- Confirmation flag
- Reason for audit trail (optional)
"""
new_owner_user_id: int = Field(
..., description="ID of the user who will become the new owner", gt=0
)
confirm_transfer: bool = Field(
..., description="Must be true to confirm ownership transfer"
)
transfer_reason: str | None = Field(
None,
max_length=500,
description="Reason for ownership transfer (for audit logs)",
)
@field_validator("confirm_transfer")
@classmethod
def validate_confirmation(cls, v):
"""Ensure confirmation is explicitly true."""
if not v:
raise ValueError("Ownership transfer requires explicit confirmation")
return v
class VendorTransferOwnershipResponse(BaseModel):
"""Response after successful ownership transfer."""
message: str
vendor_id: int
vendor_code: str
vendor_name: str
old_owner: dict[str, Any] = Field(
..., description="Information about the previous owner"
)
new_owner: dict[str, Any] = Field(
..., description="Information about the new owner"
)
transferred_at: datetime
transfer_reason: str | None