feat(vendor): add contact info inheritance from company

Vendors can now override company contact information for specific branding.
Fields are nullable - if null, value is inherited from parent company.

Database changes:
- Add vendor.contact_email, contact_phone, website, business_address, tax_number
- All nullable (null = inherit from company)
- Alembic migration: 28d44d503cac

Model changes:
- Add effective_* properties for resolved values
- Add get_contact_info_with_inheritance() helper

Schema changes:
- VendorCreate: Optional contact fields for override at creation
- VendorUpdate: Contact fields + reset_contact_to_company flag
- VendorDetailResponse: Resolved values + *_inherited flags

API changes:
- GET/PUT vendor endpoints return resolved contact info
- PUT accepts contact overrides (empty string = reset to inherit)
- _build_vendor_detail_response helper for consistent responses

Service changes:
- admin_service.update_vendor handles reset_contact_to_company flag
- Empty strings converted to None for inheritance

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 22:30:31 +01:00
parent dd51df7b31
commit 846f92e7e4
6 changed files with 506 additions and 96 deletions

View File

@@ -63,6 +63,17 @@ class Vendor(Base, TimestampMixin):
Boolean, default=False
) # Boolean to indicate if the vendor brand is verified
# ========================================================================
# Contact Information (nullable = inherit from company)
# ========================================================================
# These fields allow vendor-specific branding/identity.
# If null, the value is inherited from the parent company.
contact_email = Column(String(255), nullable=True) # Override company contact email
contact_phone = Column(String(50), nullable=True) # Override company contact phone
website = Column(String(255), nullable=True) # Override company website
business_address = Column(Text, nullable=True) # Override company business address
tax_number = Column(String(100), nullable=True) # Override company tax number
# ========================================================================
# Relationships
# ========================================================================
@@ -202,6 +213,66 @@ class Vendor(Base, TimestampMixin):
domains.append(domain.domain) # Add other active custom domains
return domains
# ========================================================================
# Contact Resolution Helper Properties
# ========================================================================
# These properties return the effective value (vendor override or company fallback)
@property
def effective_contact_email(self) -> str | None:
"""Get contact email (vendor override or company fallback)."""
if self.contact_email is not None:
return self.contact_email
return self.company.contact_email if self.company else None
@property
def effective_contact_phone(self) -> str | None:
"""Get contact phone (vendor override or company fallback)."""
if self.contact_phone is not None:
return self.contact_phone
return self.company.contact_phone if self.company else None
@property
def effective_website(self) -> str | None:
"""Get website (vendor override or company fallback)."""
if self.website is not None:
return self.website
return self.company.website if self.company else None
@property
def effective_business_address(self) -> str | None:
"""Get business address (vendor override or company fallback)."""
if self.business_address is not None:
return self.business_address
return self.company.business_address if self.company else None
@property
def effective_tax_number(self) -> str | None:
"""Get tax number (vendor override or company fallback)."""
if self.tax_number is not None:
return self.tax_number
return self.company.tax_number if self.company else None
def get_contact_info_with_inheritance(self) -> dict:
"""
Get all contact info with inheritance flags.
Returns dict with resolved values and flags indicating if inherited from company.
"""
company = self.company
return {
"contact_email": self.effective_contact_email,
"contact_email_inherited": self.contact_email is None and company is not None,
"contact_phone": self.effective_contact_phone,
"contact_phone_inherited": self.contact_phone is None and company is not None,
"website": self.effective_website,
"website_inherited": self.website is None and company is not None,
"business_address": self.effective_business_address,
"business_address_inherited": self.business_address is None and company is not None,
"tax_number": self.effective_tax_number,
"tax_number_inherited": self.tax_number is None and company is not None,
}
class VendorUserType(str, enum.Enum):
"""Types of vendor users."""

View File

@@ -25,7 +25,8 @@ class VendorCreate(BaseModel):
"""
Schema for creating a new vendor (storefront/brand) under an existing company.
Business contact info is inherited from the parent company.
Contact info is inherited from the parent company by default.
Optionally, provide contact fields to override from the start.
"""
# Parent company
@@ -51,6 +52,13 @@ class VendorCreate(BaseModel):
letzshop_csv_url_en: str | None = Field(None, description="English CSV URL")
letzshop_csv_url_de: str | None = Field(None, description="German CSV URL")
# Contact Info (optional - if not provided, inherited from company)
contact_email: str | None = Field(None, description="Override company contact email")
contact_phone: str | None = Field(None, description="Override company contact phone")
website: str | None = Field(None, description="Override company website")
business_address: str | None = Field(None, description="Override company business address")
tax_number: str | None = Field(None, description="Override company tax number")
@field_validator("subdomain")
@classmethod
def validate_subdomain(cls, v):
@@ -72,8 +80,8 @@ class VendorUpdate(BaseModel):
"""
Schema for updating vendor information (Admin only).
Note: Business contact info (contact_email, etc.) is at the Company level.
Use company update endpoints to modify those fields.
Contact fields can be overridden at the vendor level.
Set to null/empty to reset to company default (inherit).
"""
# Basic Information
@@ -90,6 +98,18 @@ class VendorUpdate(BaseModel):
is_active: bool | None = None
is_verified: bool | None = None
# Contact Info (set value to override, set to empty string to reset to inherit)
contact_email: str | None = Field(None, description="Override company contact email")
contact_phone: str | None = Field(None, description="Override company contact phone")
website: str | None = Field(None, description="Override company website")
business_address: str | None = Field(None, description="Override company business address")
tax_number: str | None = Field(None, description="Override company tax number")
# Special flag to reset contact fields to inherit from company
reset_contact_to_company: bool | None = Field(
None, description="If true, reset all contact fields to inherit from company"
)
@field_validator("subdomain")
@classmethod
def subdomain_lowercase(cls, v):
@@ -135,16 +155,14 @@ class VendorResponse(BaseModel):
class VendorDetailResponse(VendorResponse):
"""
Extended vendor response including company information.
Extended vendor response including company information and resolved contact info.
Includes company details like contact info and owner information.
Contact fields show the effective value (vendor override or company default)
with flags indicating if the value is inherited from the parent company.
"""
# 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(
@@ -152,6 +170,25 @@ class VendorDetailResponse(VendorResponse):
)
owner_username: str = Field(..., description="Username of the company owner")
# Resolved contact info (vendor override or company default)
contact_email: str | None = Field(None, description="Effective contact email")
contact_phone: str | None = Field(None, description="Effective contact phone")
website: str | None = Field(None, description="Effective website")
business_address: str | None = Field(None, description="Effective business address")
tax_number: str | None = Field(None, description="Effective tax number")
# Inheritance flags (True = value is inherited from company, not overridden)
contact_email_inherited: bool = Field(False, description="True if contact_email is from company")
contact_phone_inherited: bool = Field(False, description="True if contact_phone is from company")
website_inherited: bool = Field(False, description="True if website is from company")
business_address_inherited: bool = Field(False, description="True if business_address is from company")
tax_number_inherited: bool = Field(False, description="True if tax_number is from company")
# Original company values (for reference in UI)
company_contact_email: str | None = Field(None, description="Company's contact email")
company_contact_phone: str | None = Field(None, description="Company's phone number")
company_website: str | None = Field(None, description="Company's website URL")
class VendorCreateResponse(VendorDetailResponse):
"""