Fix some issues
This commit is contained in:
545
services/notification/app/api/public_contact.py
Normal file
545
services/notification/app/api/public_contact.py
Normal file
@@ -0,0 +1,545 @@
|
||||
# ================================================================
|
||||
# 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."
|
||||
)
|
||||
Reference in New Issue
Block a user