Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,16 @@ USE_LOCAL_STORAGE=
AI_API_BASE_URL=
AI_API_KEY=
AI_MODEL=

# E‑mail (envia mensagens para o console)
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=noreply@homematch.com

# URLs
FRONTEND_URL=http://localhost:3000
API_BASE_URL=http://localhost:8000
50 changes: 50 additions & 0 deletions apps/properties/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from django.dispatch import receiver
from apps.ai_analysis.tasks import analyze_photo_task
from .models import Properties, PropertiesPhotos
from django.utils import timezone
from apps.users.models import PropertyAlert
from apps.users.email_service import PropertyAlertEmailService
from apps.properties.filters import PropertiesFilters


# É um decorator que fica "escutando" eventos que acontecem no banco
Expand Down Expand Up @@ -41,3 +45,49 @@ def trigger_ai_analysis_on_photo_upload(
return

analyze_photo_task.delay(instance.pk, prompt)

@receiver(post_save, sender=Properties)
def check_property_alerts(sender, instance, created, **kwargs):
"""
Signal disparado após salvar um imóvel.
Verifica alertas ativos e notifica usuários cujos critérios casam com o novo imóvel.
"""
# Só processa para imóveis recém-criados
if not created:
return

# Buscar todos os alertas ativos
active_alerts = PropertyAlert.objects.filter(is_active=True).select_related('user')
for alert in active_alerts:
# Verificar se o imóvel casa com os filtros do alerta
if property_matches_alert(instance, alert.filters):
# Enviar email
success = PropertyAlertEmailService.send_property_alert_email(
user_email=alert.user.email,
user_name=alert.user.name,
property_obj=instance,
alert=alert
)
# Atualizar last_notified_at se email foi enviado
if success:
alert.last_notified_at = timezone.now()
alert.save(update_fields=['last_notified_at'])


def property_matches_alert(property_obj, filters):
"""
Verifica se um imóvel corresponde aos filtros de um alerta.

Args:
property_obj (Properties): Objeto de imóvel criado.
filters (dict): Dicionário com os filtros do alerta.

Returns:
bool: True se o imóvel casa com os filtros, False caso contrário.
"""
# Criar um queryset contendo apenas este imóvel
queryset = Properties.objects.filter(id=property_obj.id)
# Aplicar os filtros usando o PropertiesFilters
filterset = PropertiesFilters(data=filters, queryset=queryset)
# Se o queryset filtrado contém o imóvel, então ele casa com os critérios
return queryset.filter(id__in=filterset.qs.values_list('id', flat=True)).exists()
57 changes: 57 additions & 0 deletions apps/users/email_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.core.mail import send_mail
from django.conf import settings


class PropertyAlertEmailService:
"""
Serviço para envio de emails de alertas de imóveis.
"""

@staticmethod
def send_property_alert_email(user_email, user_name, property_obj, alert):
"""
Envia email notificando sobre novo imóvel que casa com os critérios do alerta.

Args:
user_email (str): Email do usuário
user_name (str): Nome do usuário
property_obj (Properties): Objeto Properties que foi criado
alert (PropertyAlert): Objeto PropertyAlert que foi ativado

Returns:
bool: True se o email foi enviado com sucesso, False caso contrário.
"""
# Construir URL do imóvel (ajustar conforme o frontend)
property_url = f"{settings.FRONTEND_URL}/properties/{property_obj.id}"

subject = f"🏠 Novo imóvel encontrado: {property_obj.get_type_display()} em {property_obj.neighborhood}"

# Mensagem em texto plano (fallback)
message = (
f"Olá {user_name},\n\n"
"Encontramos um novo imóvel que corresponde aos seus critérios de busca!\n\n"
"Detalhes do imóvel:\n"
f"- Tipo: {property_obj.get_type_display()}\n"
f"- Localização: {property_obj.neighborhood}, {property_obj.city}\n"
f"- Preço: R$ {property_obj.price}\n"
f"- Área: {property_obj.area}m²\n"
f"- Quartos: {property_obj.rooms.bedrooms}\n"
f"- Banheiros: {property_obj.rooms.bathrooms}\n\n"
f"Ver imóvel: {property_url}\n\n"
"---\n"
"Esta é uma notificação automática do sistema de alertas HomeMatch.\n"
f"Para gerenciar seus alertas, acesse: {settings.FRONTEND_URL}/alerts\n"
)

try:
send_mail(
subject=subject,
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user_email],
fail_silently=False,
)
return True
except Exception as e:
print(f"Erro ao enviar email de alerta: {e}")
return False
33 changes: 33 additions & 0 deletions apps/users/migrations/0005_propertyalert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by ChatGPT to add PropertyAlert model
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('users', '0004_user_favorites'),
]

operations = [
migrations.CreateModel(
name='PropertyAlert',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filters', models.JSONField(help_text='Filtros de busca salvos (mesmo formato da query string da API de busca)')),
('is_active', models.BooleanField(db_index=True, default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_notified_at', models.DateTimeField(blank=True, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='property_alerts', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'property_alerts',
'ordering': ['-created_at'],
},
),
migrations.AddIndex(
model_name='propertyalert',
index=models.Index(fields=['user', 'is_active'], name='property_al_user_is_active_idx'),
),
]
28 changes: 27 additions & 1 deletion apps/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,30 @@ class SearchPreference(models.Model):
neighborhood = models.CharField(max_length=100, null=True, blank=True)

class Meta:
db_table = "search_preferences"
db_table = "search_preferences"

class PropertyAlert(models.Model):
"""
Alerta de imóveis - notifica usuário quando um imóvel com os critérios especificados for cadastrado.
"""
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='property_alerts'
)
filters = models.JSONField(
help_text="Filtros de busca salvos (mesmo formato da query string da API de busca)"
)
is_active = models.BooleanField(default=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
last_notified_at = models.DateTimeField(null=True, blank=True)

class Meta:
db_table = "property_alerts"
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'is_active']),
]

def __str__(self):
return f"Alerta de {self.user.email} - {self.filters}"
23 changes: 21 additions & 2 deletions apps/users/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# apps/users/serializers.py
from rest_framework import serializers
from .models import User, SearchPreference
from .models import PropertyAlert, User, SearchPreference
from .services import UserService

class SearchPreferenceSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -33,4 +33,23 @@ def validate_email(self, value):
email = UserService.normalize_and_validate_email(value)
if email is None:
raise serializers.ValidationError("This email is currently in use.")
return email
return email


class PropertyAlertSerializer(serializers.ModelSerializer):
"""
Serializer para alertas de imóveis.
"""

class Meta:
model = PropertyAlert
fields = ['id', 'filters', 'is_active', 'created_at', 'last_notified_at']
read_only_fields = ['created_at', 'last_notified_at']

def validate_filters(self, value):
"""
Valida que o campo filters é um dicionário válido.
"""
if not isinstance(value, dict):
raise serializers.ValidationError("Os filtros devem ser um objeto JSON válido.")
return value
9 changes: 7 additions & 2 deletions apps/users/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from rest_framework.routers import DefaultRouter
from .views import UserViewSet, RegisterUserView
from .views import (UserViewSet, RegisterUserView, PasswordResetRequestView, PasswordResetConfirmView, EmailChangeRequestView, EmailChangeConfirmView,)
from django.urls import path
from rest_framework_simplejwt.views import (TokenObtainPairView, TokenRefreshView, TokenBlacklistView)

Expand All @@ -9,6 +9,11 @@
path('register/', RegisterUserView.as_view(), name='register'),
path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('logout/', TokenBlacklistView.as_view(), name='logout')
path('logout/', TokenBlacklistView.as_view(), name='logout'),
path('password-reset/', PasswordResetRequestView.as_view(), name='password_reset'),
path('password-reset/confirm/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('email-change/', EmailChangeRequestView.as_view(), name='email_change'),
path('email-change/confirm/', EmailChangeConfirmView.as_view(), name='email_change_confirm'),
path('alerts/<int:pk>/', UserViewSet.as_view({'delete': 'delete_alert'}), name='alert-delete'),
] + router.urls

Loading
Loading