fix: resolve email settings architecture violations and add tests/docs
- Fix API-002 in admin/settings.py: use service layer for DB delete - Fix API-001/API-003 in vendor/email_settings.py: add Pydantic response models, remove HTTPException raises - Fix SVC-002/SVC-006 in vendor_email_settings_service.py: use domain exceptions, change db.commit() to db.flush() - Add unit tests for VendorEmailSettingsService - Add integration tests for vendor and admin email settings APIs - Add user guide (docs/guides/email-settings.md) - Add developer guide (docs/implementation/email-settings.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -610,7 +610,8 @@ def reset_email_settings(
|
||||
for key in EMAIL_SETTING_KEYS:
|
||||
setting = admin_settings_service.get_setting_by_key(db, key)
|
||||
if setting:
|
||||
db.delete(setting)
|
||||
# Use service method for deletion (API-002 compliance)
|
||||
admin_settings_service.delete_setting(db, key, current_admin.id)
|
||||
deleted_count += 1
|
||||
|
||||
# Log action
|
||||
|
||||
167
app/api/v1/vendor/email_settings.py
vendored
167
app/api/v1/vendor/email_settings.py
vendored
@@ -14,13 +14,12 @@ Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pa
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import NotFoundError, ValidationError, AuthorizationError
|
||||
from app.services.vendor_email_settings_service import VendorEmailSettingsService
|
||||
from app.services.subscription_service import subscription_service
|
||||
from models.database.user import User
|
||||
@@ -76,16 +75,62 @@ class VerifyEmailRequest(BaseModel):
|
||||
test_email: EmailStr = Field(..., description="Email address to send test email to")
|
||||
|
||||
|
||||
# Response models for API-001 compliance
|
||||
class EmailSettingsResponse(BaseModel):
|
||||
"""Response for email settings."""
|
||||
|
||||
configured: bool
|
||||
verified: bool | None = None
|
||||
settings: dict | None = None
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class EmailStatusResponse(BaseModel):
|
||||
"""Response for email status check."""
|
||||
|
||||
is_configured: bool
|
||||
is_verified: bool
|
||||
|
||||
|
||||
class ProvidersResponse(BaseModel):
|
||||
"""Response for available providers."""
|
||||
|
||||
providers: list[dict]
|
||||
current_tier: str | None
|
||||
|
||||
|
||||
class EmailUpdateResponse(BaseModel):
|
||||
"""Response for email settings update."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
settings: dict
|
||||
|
||||
|
||||
class EmailVerifyResponse(BaseModel):
|
||||
"""Response for email verification."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
class EmailDeleteResponse(BaseModel):
|
||||
"""Response for email settings deletion."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("")
|
||||
@router.get("", response_model=EmailSettingsResponse)
|
||||
def get_email_settings(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
) -> EmailSettingsResponse:
|
||||
"""
|
||||
Get current email settings for the vendor.
|
||||
|
||||
@@ -96,24 +141,24 @@ def get_email_settings(
|
||||
|
||||
settings = service.get_settings(vendor_id)
|
||||
if not settings:
|
||||
return {
|
||||
"configured": False,
|
||||
"settings": None,
|
||||
"message": "Email settings not configured. Configure SMTP to send emails to customers.",
|
||||
}
|
||||
return EmailSettingsResponse(
|
||||
configured=False,
|
||||
settings=None,
|
||||
message="Email settings not configured. Configure SMTP to send emails to customers.",
|
||||
)
|
||||
|
||||
return {
|
||||
"configured": settings.is_configured,
|
||||
"verified": settings.is_verified,
|
||||
"settings": settings.to_dict(),
|
||||
}
|
||||
return EmailSettingsResponse(
|
||||
configured=settings.is_configured,
|
||||
verified=settings.is_verified,
|
||||
settings=settings.to_dict(),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
@router.get("/status", response_model=EmailStatusResponse)
|
||||
def get_email_status(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
) -> EmailStatusResponse:
|
||||
"""
|
||||
Get email configuration status.
|
||||
|
||||
@@ -121,14 +166,15 @@ def get_email_status(
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
service = VendorEmailSettingsService(db)
|
||||
return service.get_status(vendor_id)
|
||||
status = service.get_status(vendor_id)
|
||||
return EmailStatusResponse(**status)
|
||||
|
||||
|
||||
@router.get("/providers")
|
||||
@router.get("/providers", response_model=ProvidersResponse)
|
||||
def get_available_providers(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
) -> ProvidersResponse:
|
||||
"""
|
||||
Get available email providers for current tier.
|
||||
|
||||
@@ -140,22 +186,24 @@ def get_available_providers(
|
||||
# Get vendor's current tier
|
||||
tier = subscription_service.get_current_tier(db, vendor_id)
|
||||
|
||||
return {
|
||||
"providers": service.get_available_providers(tier),
|
||||
"current_tier": tier.value if tier else None,
|
||||
}
|
||||
return ProvidersResponse(
|
||||
providers=service.get_available_providers(tier),
|
||||
current_tier=tier.value if tier else None,
|
||||
)
|
||||
|
||||
|
||||
@router.put("")
|
||||
@router.put("", response_model=EmailUpdateResponse)
|
||||
def update_email_settings(
|
||||
data: EmailSettingsUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
) -> EmailUpdateResponse:
|
||||
"""
|
||||
Create or update email settings.
|
||||
|
||||
Premium providers (SendGrid, Mailgun, SES) require Business+ tier.
|
||||
Raises AuthorizationException if tier is insufficient.
|
||||
Raises ValidationException if data is invalid.
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
service = VendorEmailSettingsService(db)
|
||||
@@ -163,63 +211,66 @@ def update_email_settings(
|
||||
# Get vendor's current tier for validation
|
||||
tier = subscription_service.get_current_tier(db, vendor_id)
|
||||
|
||||
try:
|
||||
settings = service.create_or_update(
|
||||
vendor_id=vendor_id,
|
||||
data=data.model_dump(exclude_unset=True),
|
||||
current_tier=tier,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Email settings updated successfully",
|
||||
"settings": settings.to_dict(),
|
||||
}
|
||||
except AuthorizationError as e:
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
# Service raises appropriate exceptions (API-003 compliance)
|
||||
settings = service.create_or_update(
|
||||
vendor_id=vendor_id,
|
||||
data=data.model_dump(exclude_unset=True),
|
||||
current_tier=tier,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return EmailUpdateResponse(
|
||||
success=True,
|
||||
message="Email settings updated successfully",
|
||||
settings=settings.to_dict(),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/verify")
|
||||
@router.post("/verify", response_model=EmailVerifyResponse)
|
||||
def verify_email_settings(
|
||||
data: VerifyEmailRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
) -> EmailVerifyResponse:
|
||||
"""
|
||||
Verify email settings by sending a test email.
|
||||
|
||||
Sends a test email to the provided address and updates verification status.
|
||||
Raises ResourceNotFoundException if settings not configured.
|
||||
Raises ValidationException if verification fails.
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
service = VendorEmailSettingsService(db)
|
||||
|
||||
try:
|
||||
result = service.verify_settings(vendor_id, data.test_email)
|
||||
if result["success"]:
|
||||
return result
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result["message"])
|
||||
except NotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
# Service raises appropriate exceptions (API-003 compliance)
|
||||
result = service.verify_settings(vendor_id, data.test_email)
|
||||
db.commit()
|
||||
|
||||
return EmailVerifyResponse(
|
||||
success=result["success"],
|
||||
message=result["message"],
|
||||
)
|
||||
|
||||
|
||||
@router.delete("")
|
||||
@router.delete("", response_model=EmailDeleteResponse)
|
||||
def delete_email_settings(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
) -> EmailDeleteResponse:
|
||||
"""
|
||||
Delete email settings.
|
||||
|
||||
Warning: This will disable email sending for the vendor.
|
||||
Raises ResourceNotFoundException if settings not found.
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
service = VendorEmailSettingsService(db)
|
||||
|
||||
if service.delete(vendor_id):
|
||||
return {"success": True, "message": "Email settings deleted"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Email settings not found")
|
||||
# Service raises ResourceNotFoundException if not found (API-003 compliance)
|
||||
service.delete(vendor_id)
|
||||
db.commit()
|
||||
|
||||
return EmailDeleteResponse(
|
||||
success=True,
|
||||
message="Email settings deleted",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user