Files
bakery-ia/services/notification/app/api/public_contact.py

546 lines
19 KiB
Python
Raw Normal View History

2026-01-25 20:07:37 +01:00
# ================================================================
# services/notification/app/api/public_contact.py
# ================================================================
"""
Public contact form API endpoints
Handles contact, feedback, and prelaunch email submissions
These endpoints are public (no authentication required)
"""
import structlog
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, EmailStr, Field
from typing import Optional, Literal
from datetime import datetime
from app.services.email_service import EmailService
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter(prefix="/api/v1/public", tags=["public-contact"])
# ================================================================
# PYDANTIC MODELS
# ================================================================
class ContactFormRequest(BaseModel):
"""Contact form submission request"""
name: str = Field(..., min_length=2, max_length=100)
email: EmailStr
phone: Optional[str] = Field(None, max_length=20)
bakery_name: Optional[str] = Field(None, max_length=100)
type: Literal["general", "technical", "sales", "feedback"] = "general"
subject: str = Field(..., min_length=5, max_length=200)
message: str = Field(..., min_length=10, max_length=5000)
class FeedbackFormRequest(BaseModel):
"""Feedback form submission request"""
name: str = Field(..., min_length=2, max_length=100)
email: EmailStr
category: Literal["suggestion", "bug", "feature", "praise", "complaint"]
title: str = Field(..., min_length=5, max_length=200)
description: str = Field(..., min_length=10, max_length=5000)
rating: Optional[int] = Field(None, ge=1, le=5)
class PrelaunchEmailRequest(BaseModel):
"""Prelaunch email subscription request"""
email: EmailStr
class ContactFormResponse(BaseModel):
"""Response for form submissions"""
success: bool
message: str
# ================================================================
# EMAIL TEMPLATES
# ================================================================
CONTACT_EMAIL_TEMPLATE_TEXT = """
Nuevo mensaje de contacto recibido
==============================================
DATOS DEL CONTACTO
==============================================
Nombre: {name}
Email: {email}
Teléfono: {phone}
Panadería: {bakery_name}
Tipo de consulta: {type_label}
==============================================
ASUNTO
==============================================
{subject}
==============================================
MENSAJE
==============================================
{message}
==============================================
Recibido: {timestamp}
"""
CONTACT_EMAIL_TEMPLATE_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
.header {{ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); padding: 30px; text-align: center; }}
.header h1 {{ color: white; margin: 0; font-size: 24px; }}
.content {{ padding: 30px; }}
.info-box {{ background-color: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; }}
.info-row {{ display: flex; margin-bottom: 10px; }}
.info-label {{ font-weight: bold; color: #92400e; min-width: 120px; }}
.info-value {{ color: #374151; }}
.message-box {{ background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
.message-box h3 {{ color: #1f2937; margin-top: 0; }}
.footer {{ background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }}
.type-badge {{ display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; }}
.type-general {{ background-color: #dbeafe; color: #1e40af; }}
.type-technical {{ background-color: #fce7f3; color: #9d174d; }}
.type-sales {{ background-color: #d1fae5; color: #065f46; }}
.type-feedback {{ background-color: #e0e7ff; color: #3730a3; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nuevo Mensaje de Contacto</h1>
</div>
<div class="content">
<div class="info-box">
<div class="info-row">
<span class="info-label">Nombre:</span>
<span class="info-value">{name}</span>
</div>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value"><a href="mailto:{email}">{email}</a></span>
</div>
<div class="info-row">
<span class="info-label">Teléfono:</span>
<span class="info-value">{phone}</span>
</div>
<div class="info-row">
<span class="info-label">Panadería:</span>
<span class="info-value">{bakery_name}</span>
</div>
<div class="info-row">
<span class="info-label">Tipo:</span>
<span class="info-value"><span class="type-badge type-{type}">{type_label}</span></span>
</div>
</div>
<div class="message-box">
<h3>{subject}</h3>
<p style="white-space: pre-wrap; color: #374151;">{message}</p>
</div>
</div>
<div class="footer">
Recibido: {timestamp}<br>
BakeWise - Sistema de Contacto
</div>
</div>
</body>
</html>
"""
FEEDBACK_EMAIL_TEMPLATE_TEXT = """
Nuevo feedback recibido
==============================================
DATOS DEL USUARIO
==============================================
Nombre: {name}
Email: {email}
Categoría: {category_label}
Valoración: {rating_display}
==============================================
TÍTULO
==============================================
{title}
==============================================
DESCRIPCIÓN
==============================================
{description}
==============================================
Recibido: {timestamp}
"""
FEEDBACK_EMAIL_TEMPLATE_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
.header {{ background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); padding: 30px; text-align: center; }}
.header h1 {{ color: white; margin: 0; font-size: 24px; }}
.content {{ padding: 30px; }}
.info-box {{ background-color: #ede9fe; border-left: 4px solid #8b5cf6; padding: 15px; margin: 20px 0; }}
.info-row {{ display: flex; margin-bottom: 10px; }}
.info-label {{ font-weight: bold; color: #5b21b6; min-width: 120px; }}
.info-value {{ color: #374151; }}
.message-box {{ background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
.message-box h3 {{ color: #1f2937; margin-top: 0; }}
.footer {{ background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }}
.category-badge {{ display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; }}
.category-suggestion {{ background-color: #dbeafe; color: #1e40af; }}
.category-bug {{ background-color: #fee2e2; color: #991b1b; }}
.category-feature {{ background-color: #d1fae5; color: #065f46; }}
.category-praise {{ background-color: #fef3c7; color: #92400e; }}
.category-complaint {{ background-color: #fce7f3; color: #9d174d; }}
.stars {{ color: #f59e0b; font-size: 18px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nuevo Feedback Recibido</h1>
</div>
<div class="content">
<div class="info-box">
<div class="info-row">
<span class="info-label">Nombre:</span>
<span class="info-value">{name}</span>
</div>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value"><a href="mailto:{email}">{email}</a></span>
</div>
<div class="info-row">
<span class="info-label">Categoría:</span>
<span class="info-value"><span class="category-badge category-{category}">{category_label}</span></span>
</div>
<div class="info-row">
<span class="info-label">Valoración:</span>
<span class="info-value stars">{rating_display}</span>
</div>
</div>
<div class="message-box">
<h3>{title}</h3>
<p style="white-space: pre-wrap; color: #374151;">{description}</p>
</div>
</div>
<div class="footer">
Recibido: {timestamp}<br>
BakeWise - Sistema de Feedback
</div>
</div>
</body>
</html>
"""
PRELAUNCH_EMAIL_TEMPLATE_TEXT = """
Nueva suscripción de pre-lanzamiento
==============================================
NUEVO INTERESADO
==============================================
Email: {email}
El usuario ha mostrado interés en BakeWise y quiere ser notificado cuando se lance oficialmente.
==============================================
Recibido: {timestamp}
"""
PRELAUNCH_EMAIL_TEMPLATE_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
.header {{ background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; }}
.header h1 {{ color: white; margin: 0; font-size: 24px; }}
.content {{ padding: 30px; text-align: center; }}
.email-box {{ background-color: #d1fae5; border: 2px solid #10b981; border-radius: 12px; padding: 20px; margin: 20px 0; }}
.email-box h2 {{ color: #065f46; margin: 0 0 10px 0; }}
.email-box a {{ color: #059669; font-size: 18px; font-weight: bold; }}
.footer {{ background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }}
.rocket {{ font-size: 48px; margin-bottom: 10px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nueva Suscripción Pre-Lanzamiento</h1>
</div>
<div class="content">
<div class="rocket">🚀</div>
<p style="color: #374151; font-size: 16px;">Un nuevo usuario quiere ser notificado del lanzamiento de BakeWise</p>
<div class="email-box">
<h2>Email del interesado:</h2>
<a href="mailto:{email}">{email}</a>
</div>
<p style="color: #6b7280;">Añade este email a la lista de espera para el lanzamiento oficial.</p>
</div>
<div class="footer">
Recibido: {timestamp}<br>
BakeWise - Lista de Espera
</div>
</div>
</body>
</html>
"""
# ================================================================
# HELPER FUNCTIONS
# ================================================================
def get_type_label(type_code: str) -> str:
"""Get human-readable label for contact type"""
labels = {
"general": "Consulta General",
"technical": "Soporte Técnico",
"sales": "Ventas",
"feedback": "Feedback"
}
return labels.get(type_code, type_code)
def get_category_label(category: str) -> str:
"""Get human-readable label for feedback category"""
labels = {
"suggestion": "Sugerencia",
"bug": "Error/Bug",
"feature": "Nueva Funcionalidad",
"praise": "Elogio",
"complaint": "Queja"
}
return labels.get(category, category)
def get_rating_display(rating: Optional[int]) -> str:
"""Convert rating to star display"""
if rating is None:
return "N/A"
return "" * rating + "" * (5 - rating)
# ================================================================
# API ENDPOINTS
# ================================================================
@router.post("/contact", response_model=ContactFormResponse)
async def submit_contact_form(request: ContactFormRequest):
"""
Submit a contact form message.
Sends an email to contact@bakewise.ai with the form data.
"""
try:
email_service = EmailService()
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
type_label = get_type_label(request.type)
# Format email content
text_content = CONTACT_EMAIL_TEMPLATE_TEXT.format(
name=request.name,
email=request.email,
phone=request.phone or "No proporcionado",
bakery_name=request.bakery_name or "No proporcionado",
type=request.type,
type_label=type_label,
subject=request.subject,
message=request.message,
timestamp=timestamp
)
html_content = CONTACT_EMAIL_TEMPLATE_HTML.format(
name=request.name,
email=request.email,
phone=request.phone or "No proporcionado",
bakery_name=request.bakery_name or "No proporcionado",
type=request.type,
type_label=type_label,
subject=request.subject,
message=request.message,
timestamp=timestamp
)
# Send email to contact@bakewise.ai
success = await email_service.send_email(
to_email="contact@bakewise.ai",
subject=f"[{type_label}] {request.subject}",
text_content=text_content,
html_content=html_content,
reply_to=request.email
)
if success:
logger.info("Contact form submitted successfully",
from_email=request.email,
type=request.type,
subject=request.subject)
return ContactFormResponse(
success=True,
message="Mensaje enviado correctamente. Te responderemos pronto."
)
else:
logger.error("Failed to send contact form email",
from_email=request.email)
raise HTTPException(
status_code=500,
detail="Error al enviar el mensaje. Por favor, inténtalo de nuevo."
)
except HTTPException:
raise
except Exception as e:
logger.error("Contact form submission error", error=str(e))
raise HTTPException(
status_code=500,
detail="Error interno. Por favor, inténtalo más tarde."
)
@router.post("/feedback", response_model=ContactFormResponse)
async def submit_feedback_form(request: FeedbackFormRequest):
"""
Submit a feedback form.
Sends an email to contact@bakewise.ai with the feedback data.
"""
try:
email_service = EmailService()
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
category_label = get_category_label(request.category)
rating_display = get_rating_display(request.rating)
# Format email content
text_content = FEEDBACK_EMAIL_TEMPLATE_TEXT.format(
name=request.name,
email=request.email,
category=request.category,
category_label=category_label,
rating_display=rating_display,
title=request.title,
description=request.description,
timestamp=timestamp
)
html_content = FEEDBACK_EMAIL_TEMPLATE_HTML.format(
name=request.name,
email=request.email,
category=request.category,
category_label=category_label,
rating_display=rating_display,
title=request.title,
description=request.description,
timestamp=timestamp
)
# Send email to contact@bakewise.ai
success = await email_service.send_email(
to_email="contact@bakewise.ai",
subject=f"[Feedback - {category_label}] {request.title}",
text_content=text_content,
html_content=html_content,
reply_to=request.email
)
if success:
logger.info("Feedback form submitted successfully",
from_email=request.email,
category=request.category,
title=request.title)
return ContactFormResponse(
success=True,
message="Gracias por tu feedback. Lo revisaremos pronto."
)
else:
logger.error("Failed to send feedback form email",
from_email=request.email)
raise HTTPException(
status_code=500,
detail="Error al enviar el feedback. Por favor, inténtalo de nuevo."
)
except HTTPException:
raise
except Exception as e:
logger.error("Feedback form submission error", error=str(e))
raise HTTPException(
status_code=500,
detail="Error interno. Por favor, inténtalo más tarde."
)
@router.post("/prelaunch-subscribe", response_model=ContactFormResponse)
async def submit_prelaunch_email(request: PrelaunchEmailRequest):
"""
Submit a prelaunch email subscription.
Sends a notification email to contact@bakewise.ai.
"""
try:
email_service = EmailService()
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
# Format email content
text_content = PRELAUNCH_EMAIL_TEMPLATE_TEXT.format(
email=request.email,
timestamp=timestamp
)
html_content = PRELAUNCH_EMAIL_TEMPLATE_HTML.format(
email=request.email,
timestamp=timestamp
)
# Send email to contact@bakewise.ai
success = await email_service.send_email(
to_email="contact@bakewise.ai",
subject=f"[Pre-Lanzamiento] Nueva suscripción: {request.email}",
text_content=text_content,
html_content=html_content,
reply_to=request.email
)
if success:
logger.info("Prelaunch subscription submitted successfully",
email=request.email)
return ContactFormResponse(
success=True,
message="Te hemos apuntado a la lista de espera."
)
else:
logger.error("Failed to send prelaunch subscription email",
email=request.email)
raise HTTPException(
status_code=500,
detail="Error al registrar tu email. Por favor, inténtalo de nuevo."
)
except HTTPException:
raise
except Exception as e:
logger.error("Prelaunch subscription error", error=str(e))
raise HTTPException(
status_code=500,
detail="Error interno. Por favor, inténtalo más tarde."
)