546 lines
19 KiB
Python
546 lines
19 KiB
Python
# ================================================================
|
|
# 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 ttt"""
|
|
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."
|
|
)
|