diff --git a/models/schema/company.py b/models/schema/company.py index b8a4a035..b7d7b55d 100644 --- a/models/schema/company.py +++ b/models/schema/company.py @@ -5,6 +5,9 @@ Pydantic schemas for Company model. 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 @@ -106,16 +109,24 @@ class CompanyResponse(BaseModel): 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. """ + # 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") active_vendor_count: int = Field( 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): """Schema for paginated company list.""" @@ -153,3 +164,53 @@ class CompanySummary(BaseModel): is_active: bool is_verified: bool 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 diff --git a/models/schema/vendor.py b/models/schema/vendor.py index 4e43a11e..be35f881 100644 --- a/models/schema/vendor.py +++ b/models/schema/vendor.py @@ -3,25 +3,33 @@ Pydantic schemas for Vendor-related operations. Schemas include: -- VendorCreate: For creating vendors with owner accounts +- VendorCreate: For creating vendors under companies - VendorUpdate: For updating vendor information (Admin only) - VendorResponse: Standard vendor response -- VendorDetailResponse: Vendor response with owner details -- VendorCreateResponse: Response after vendor creation (includes credentials) +- VendorDetailResponse: Vendor response with company/owner details +- VendorCreateResponse: Response after vendor creation - VendorListResponse: Paginated vendor list - 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 from datetime import datetime -from typing import Any from pydantic import BaseModel, ConfigDict, Field, field_validator 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 vendor_code: str = Field( @@ -34,41 +42,15 @@ class VendorCreate(BaseModel): ..., description="Unique subdomain for the vendor", min_length=2, max_length=100 ) 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) - 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) + # Marketplace URLs (brand-specific multi-language support) 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_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") @classmethod def validate_subdomain(cls, v): @@ -90,8 +72,8 @@ class VendorUpdate(BaseModel): """ Schema for updating vendor information (Admin only). - Note: owner_email is NOT included here. To change the owner, - use the transfer-ownership endpoint instead. + Note: Business contact info (contact_email, etc.) is at the Company level. + Use company update endpoints to modify those fields. """ # Basic Information @@ -99,16 +81,7 @@ class VendorUpdate(BaseModel): description: str | None = None subdomain: str | None = Field(None, min_length=2, max_length=100) - # Business Contact Information (Vendor Fields) - 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 + # Marketplace URLs (brand-specific) letzshop_csv_url_fr: str | None = None letzshop_csv_url_en: str | None = None letzshop_csv_url_de: str | None = None @@ -123,19 +96,17 @@ class VendorUpdate(BaseModel): """Normalize subdomain to lowercase.""" 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) 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) @@ -144,18 +115,11 @@ class VendorResponse(BaseModel): subdomain: str name: str description: str | None - owner_user_id: int - # Contact Information (Business) - contact_email: str | None - contact_phone: str | None - website: str | None + # Company relationship + company_id: int - # Business Information - business_address: str | None - tax_number: str | None - - # Marketplace URLs + # Marketplace URLs (brand-specific) letzshop_csv_url_fr: str | None letzshop_csv_url_en: str | None letzshop_csv_url_de: str | None @@ -171,30 +135,33 @@ class VendorResponse(BaseModel): class VendorDetailResponse(VendorResponse): """ - Extended vendor response including owner information. + Extended vendor response including company information. - Includes both: - - contact_email (business contact) - - owner_email (owner's authentication email) + Includes company details like contact info and owner information. """ + # 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( - ..., 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): """ - 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( - ..., description="Temporary password for owner (SHOWN ONLY ONCE)" - ) - login_url: str | None = Field(None, description="URL for vendor owner to login") + login_url: str | None = Field(None, description="URL for vendor storefront") class VendorListResponse(BaseModel): @@ -215,55 +182,10 @@ class VendorSummary(BaseModel): vendor_code: str subdomain: str name: str + company_id: int is_active: bool -class VendorTransferOwnership(BaseModel): - """ - Schema for transferring vendor 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 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 +# NOTE: Vendor ownership transfer schemas have been removed. +# Ownership transfer is now handled at the Company level. +# See models/schema/company.py for CompanyTransferOwnership and CompanyTransferOwnershipResponse.