# Vendor Contact Inheritance Migration ## Overview **Feature:** Add contact information fields to Vendor model with inheritance from Company. **Pattern:** Nullable with Fallback - Vendor fields are nullable; if null, inherit from parent company at read time. **Benefits:** - Vendors inherit company contact info by default - Can override specific fields for vendor-specific branding/identity - Can reset to "inherit from company" by setting field to null - Company updates automatically reflect in vendors that haven't overridden --- ## Database Changes ### New Columns in `vendor` Table | Column | Type | Nullable | Default | Description | |--------|------|----------|---------|-------------| | `contact_email` | VARCHAR(255) | Yes | NULL | Override company contact email | | `contact_phone` | VARCHAR(50) | Yes | NULL | Override company contact phone | | `website` | VARCHAR(255) | Yes | NULL | Override company website | | `business_address` | TEXT | Yes | NULL | Override company business address | | `tax_number` | VARCHAR(100) | Yes | NULL | Override company tax number | ### Resolution Logic ``` effective_value = vendor.field if vendor.field is not None else vendor.company.field ``` --- ## Files to Modify ### 1. Database Model - `models/database/vendor.py` - Add nullable contact fields ### 2. Alembic Migration - `alembic/versions/xxx_add_vendor_contact_fields.py` - New migration ### 3. Pydantic Schemas - `models/schema/vendor.py`: - `VendorUpdate` - Add optional contact fields - `VendorResponse` - Add resolved contact fields - `VendorDetailResponse` - Add contact fields with inheritance indicator ### 4. Service Layer - `app/services/vendor_service.py` - Add contact resolution helper - `app/services/admin_service.py` - Update create/update to handle contact fields ### 5. API Endpoints - `app/api/v1/admin/vendors.py` - Update responses to include resolved contact info ### 6. Frontend - `app/templates/admin/vendor-edit.html` - Add contact fields with inheritance toggle - `static/admin/js/vendor-edit.js` - Handle inheritance UI logic --- ## Implementation Steps ### Step 1: Database Model ```python # models/database/vendor.py class Vendor(Base): # ... existing fields ... # Contact fields (nullable = inherit from company) contact_email = Column(String(255), nullable=True) contact_phone = Column(String(50), nullable=True) website = Column(String(255), nullable=True) business_address = Column(Text, nullable=True) tax_number = Column(String(100), nullable=True) # Helper properties for resolved values @property def effective_contact_email(self) -> str | None: return self.contact_email if self.contact_email is not None else ( self.company.contact_email if self.company else None ) @property def effective_contact_phone(self) -> str | None: return self.contact_phone if self.contact_phone is not None else ( self.company.contact_phone if self.company else None ) # ... similar for other fields ... ``` ### Step 2: Alembic Migration ```python def upgrade(): op.add_column('vendor', sa.Column('contact_email', sa.String(255), nullable=True)) op.add_column('vendor', sa.Column('contact_phone', sa.String(50), nullable=True)) op.add_column('vendor', sa.Column('website', sa.String(255), nullable=True)) op.add_column('vendor', sa.Column('business_address', sa.Text(), nullable=True)) op.add_column('vendor', sa.Column('tax_number', sa.String(100), nullable=True)) def downgrade(): op.drop_column('vendor', 'tax_number') op.drop_column('vendor', 'business_address') op.drop_column('vendor', 'website') op.drop_column('vendor', 'contact_phone') op.drop_column('vendor', 'contact_email') ``` ### Step 3: Pydantic Schemas ```python # models/schema/vendor.py class VendorUpdate(BaseModel): # ... existing fields ... # Contact fields (None = don't update, empty string could mean "clear/inherit") contact_email: str | None = None contact_phone: str | None = None website: str | None = None business_address: str | None = None tax_number: str | None = None class VendorContactInfo(BaseModel): """Resolved contact information with inheritance indicators.""" contact_email: str | None contact_phone: str | None website: str | None business_address: str | None tax_number: str | None # Inheritance flags contact_email_inherited: bool = False contact_phone_inherited: bool = False website_inherited: bool = False business_address_inherited: bool = False tax_number_inherited: bool = False class VendorDetailResponse(BaseModel): # ... existing fields ... # Resolved contact info contact_email: str | None contact_phone: str | None website: str | None business_address: str | None tax_number: str | None # Inheritance indicators (for UI) contact_email_inherited: bool contact_phone_inherited: bool website_inherited: bool business_address_inherited: bool tax_number_inherited: bool ``` ### Step 4: Service Layer Helper ```python # app/services/vendor_service.py def get_resolved_contact_info(self, vendor: Vendor) -> dict: """ Get resolved contact information with inheritance flags. Returns dict with both values and flags indicating if inherited. """ company = vendor.company return { "contact_email": vendor.contact_email or (company.contact_email if company else None), "contact_email_inherited": vendor.contact_email is None and company is not None, "contact_phone": vendor.contact_phone or (company.contact_phone if company else None), "contact_phone_inherited": vendor.contact_phone is None and company is not None, "website": vendor.website or (company.website if company else None), "website_inherited": vendor.website is None and company is not None, "business_address": vendor.business_address or (company.business_address if company else None), "business_address_inherited": vendor.business_address is None and company is not None, "tax_number": vendor.tax_number or (company.tax_number if company else None), "tax_number_inherited": vendor.tax_number is None and company is not None, } ``` ### Step 5: API Endpoint Updates ```python # app/api/v1/admin/vendors.py @router.get("/{vendor_identifier}", response_model=VendorDetailResponse) def get_vendor_details(...): vendor = vendor_service.get_vendor_by_identifier(db, vendor_identifier) contact_info = vendor_service.get_resolved_contact_info(vendor) return VendorDetailResponse( # ... existing fields ... **contact_info, # Includes values and inheritance flags ) ``` ### Step 6: Frontend UI ```html ``` --- ## API Behavior ### GET /api/v1/admin/vendors/{id} Returns resolved contact info with inheritance flags: ```json { "id": 1, "vendor_code": "VENDOR001", "name": "My Vendor", "contact_email": "sales@company.com", "contact_email_inherited": true, "contact_phone": "+352 123 456", "contact_phone_inherited": false, "website": "https://company.com", "website_inherited": true } ``` ### PUT /api/v1/admin/vendors/{id} To override a field: ```json { "contact_email": "vendor-specific@example.com" } ``` To reset to inherit from company (set to null): ```json { "contact_email": null } ``` --- ## Testing Plan 1. **Create vendor** - Verify contact fields are null (inheriting) 2. **Read vendor** - Verify resolved values come from company 3. **Update vendor contact** - Verify override works 4. **Reset to inherit** - Verify setting null restores inheritance 5. **Update company** - Verify change reflects in inheriting vendors 6. **Update company** - Verify change does NOT affect overridden vendors --- ## Rollback Plan If issues occur: 1. Run downgrade migration: `alembic downgrade -1` 2. Revert code changes 3. Re-deploy --- ## Progress Tracking - [ ] Database model updated - [ ] Alembic migration created and applied - [ ] Pydantic schemas updated - [ ] Service layer helper added - [ ] API endpoints updated - [ ] Frontend forms updated - [ ] Tests written and passing - [ ] Documentation updated