2026-08-24 15:54:08 +03:30
|
|
|
from django.db.models import Count
|
|
|
|
|
from rest_framework import status
|
|
|
|
|
from rest_framework.exceptions import NotFound, ValidationError
|
|
|
|
|
from rest_framework.generics import RetrieveAPIView
|
|
|
|
|
from rest_framework.permissions import IsAuthenticated
|
|
|
|
|
from rest_framework.response import Response
|
|
|
|
|
from rest_framework.views import APIView
|
|
|
|
|
|
|
|
|
|
from apps.banker.models import ContactGroup
|
|
|
|
|
from apps.billing.permissions import HasActiveFeature
|
|
|
|
|
from apps.messaging.models import BulkMessage, MessageRecipient
|
|
|
|
|
|
2026-09-04 13:05:54 +03:30
|
|
|
from apps.billing.forms import normalize_iran_phone
|
|
|
|
|
|
|
|
|
|
from .serializers import (
|
|
|
|
|
BulkMessageSerializer,
|
|
|
|
|
ContactMessageHistorySerializer,
|
|
|
|
|
SendBulkMessageSerializer,
|
|
|
|
|
)
|
2026-08-24 15:54:08 +03:30
|
|
|
from .services import BulkMessageService
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendBulkMessageView(APIView):
|
|
|
|
|
"""POST: میسازد و بلافاصله dispatch میکند (پیامک/کانالهای Business-API-دار
|
|
|
|
|
خودکار، بقیه needs_manual برای صف deep-link سمت اپ)."""
|
|
|
|
|
|
|
|
|
|
permission_classes = [IsAuthenticated, HasActiveFeature]
|
|
|
|
|
feature_key = "sms_bank"
|
|
|
|
|
throttle_scope = "messaging_send"
|
|
|
|
|
|
|
|
|
|
def post(self, request):
|
|
|
|
|
serializer = SendBulkMessageSerializer(data=request.data)
|
|
|
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
|
data = serializer.validated_data
|
|
|
|
|
|
|
|
|
|
if data["group_id"] and not ContactGroup.objects.filter(user=request.user, id=data["group_id"]).exists():
|
|
|
|
|
raise ValidationError({"group_id": "گروه یافت نشد."})
|
|
|
|
|
|
|
|
|
|
bulk = BulkMessageService.create_and_dispatch(
|
|
|
|
|
user=request.user,
|
|
|
|
|
channels=data["channels"],
|
|
|
|
|
group_id=data["group_id"],
|
|
|
|
|
contact_ids=data["contact_ids"],
|
|
|
|
|
template_id=data["template_id"],
|
|
|
|
|
body_override=data["body_override"],
|
|
|
|
|
)
|
|
|
|
|
return Response(BulkMessageSerializer(bulk).data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BulkMessageDetailView(RetrieveAPIView):
|
|
|
|
|
serializer_class = BulkMessageSerializer
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
|
return BulkMessage.objects.filter(user=self.request.user)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MessagingStatsView(APIView):
|
|
|
|
|
"""آمار ارسال: چند پیام موفق به گیتوی/API فرستاده شده، چند تا ناموفق
|
|
|
|
|
بوده، چند تا نیاز به تایید دستی داشته/دارد — بر اساس MessageRecipient
|
|
|
|
|
(نه رسید تحویل واقعی گوشی، چون آن نیاز به webhook از سرویسدهندهی
|
|
|
|
|
پیامک دارد که در این نسخه وصل نشده؛ «ارسالشده» یعنی سرور با موفقیت آن
|
|
|
|
|
را به API/گیتوی تحویل داده)."""
|
|
|
|
|
|
|
|
|
|
permission_classes = [IsAuthenticated, HasActiveFeature]
|
|
|
|
|
feature_key = "sms_bank"
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
rows = (
|
|
|
|
|
MessageRecipient.objects.filter(bulk_message__user=request.user)
|
|
|
|
|
.values("status")
|
|
|
|
|
.annotate(count=Count("id"))
|
|
|
|
|
)
|
|
|
|
|
counts = {row["status"]: row["count"] for row in rows}
|
|
|
|
|
total = sum(counts.values())
|
|
|
|
|
return Response(
|
|
|
|
|
{
|
|
|
|
|
"total": total,
|
|
|
|
|
"sent": counts.get(MessageRecipient.Status.SENT, 0),
|
|
|
|
|
"failed": counts.get(MessageRecipient.Status.FAILED, 0),
|
|
|
|
|
"needs_manual": counts.get(MessageRecipient.Status.NEEDS_MANUAL, 0),
|
|
|
|
|
"manual_confirmed": counts.get(MessageRecipient.Status.MANUAL_CONFIRMED, 0),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConfirmManualSendView(APIView):
|
|
|
|
|
"""اپ بعد از اینکه کاربر دستی در واتساپ/تلگرام/بله/ایتا زد «ارسال»، این را صدا میزند."""
|
|
|
|
|
|
|
|
|
|
permission_classes = [IsAuthenticated, HasActiveFeature]
|
|
|
|
|
feature_key = "sms_bank"
|
|
|
|
|
|
|
|
|
|
def post(self, request, bulk_id, recipient_id):
|
|
|
|
|
try:
|
|
|
|
|
recipient = BulkMessageService.confirm_manual(request.user, bulk_id, recipient_id)
|
|
|
|
|
except MessageRecipient.DoesNotExist:
|
|
|
|
|
raise NotFound("گیرنده یافت نشد.")
|
|
|
|
|
return Response({"success": True, "status": recipient.status})
|
2026-09-04 13:05:54 +03:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class ContactMessageHistoryView(APIView):
|
|
|
|
|
"""تاریخچهی پیامهای فرستادهشده به یک شماره.
|
|
|
|
|
|
|
|
|
|
چرا بر اساس *شماره* و نه شناسهی مخاطب: صفحهی «مشتریان» و «مخاطبین»
|
|
|
|
|
دو موجودیتِ جدا هستند و کاربر انتظار دارد روی پروفایلِ مشتری هم ببیند
|
|
|
|
|
چه پیامی برایش رفته. شماره تنها چیزی است که این دو را به هم وصل
|
|
|
|
|
میکند، پس با شمارهی نرمالشده جستجو میکنیم.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
raw = request.query_params.get("phone", "") or ""
|
|
|
|
|
phone = normalize_iran_phone(raw)
|
|
|
|
|
if not phone:
|
|
|
|
|
raise ValidationError({"phone": "شماره لازم است."})
|
|
|
|
|
|
|
|
|
|
# ارقامِ ذخیرهشده ممکن است با ۰ یا +۹۸ باشند؛ روی چند شکل تطبیق
|
|
|
|
|
# میدهیم تا تاریخچه بهخاطر تفاوتِ قالبِ شماره گم نشود.
|
|
|
|
|
variants = {phone, phone.lstrip("0"), "+98" + phone.lstrip("0"), "98" + phone.lstrip("0")}
|
|
|
|
|
rows = (
|
|
|
|
|
MessageRecipient.objects.filter(
|
|
|
|
|
bulk_message__user=request.user,
|
|
|
|
|
contact__phone__in=variants,
|
|
|
|
|
)
|
|
|
|
|
.select_related("bulk_message")
|
|
|
|
|
.order_by("-bulk_message__created_at", "-id")[:100]
|
|
|
|
|
)
|
|
|
|
|
return Response(ContactMessageHistorySerializer(rows, many=True).data)
|