import random import re import string import uuid from django.db import transaction from apps.account.models import User from apps.banker.models import Contact from apps.messaging.api.services import BulkMessageService from apps.messaging.models import Channel, MessageRecipient from apps.orders.models import Order, OrderStage, OrderStageLog _FIELD_PATTERN = re.compile(r"\{(.*?)\}") _TRACKING_CODE_ALPHABET = string.ascii_uppercase + string.digits def generate_tracking_code() -> str: """کد پیگیریِ کوتاه و خوانا برای مشتری — نه شماره‌ی پستیِ واقعی (آن را وقتی از پستخانه گرفت دستی جایگزین می‌کند)، بلکه یک مرجع سریع تا از همان مرحله‌ی اول سفارش یک کدِ پیگیری داشته باشد.""" return "TRK-" + "".join(random.choices(_TRACKING_CODE_ALPHABET, k=6)) class OrderStageService: @staticmethod def ensure_default_stage(user: User) -> None: """اگر کاربر هنوز هیچ مرحله‌ای تعریف نکرده، یک مرحله‌ی پیش‌فرض می‌سازد تا لیستِ مراحل هیچ‌وقت کاملا خالی نباشد و اولین سفارش از همان روزِ اول قابل‌پیگیری باشد. بدونِ قالبِ پیامک (کاربر خودش بعدا یکی وصل می‌کند) — چون قالبِ پیشِ‌فرضی که خودمان بسازیم به احتمال زیاد با کسب‌وکارش نمی‌خواند.""" if OrderStage.objects.filter(user=user).exists(): return OrderStage.objects.create( id=str(uuid.uuid4()), user=user, label="ثبت سفارش", sort_order=0, ) @staticmethod def _render(order: Order, stage: OrderStage, extra_input_value: str = "") -> str: body = stage.template.body if stage.template else "" values = { "نام": order.customer_name, "شماره": order.customer_phone, "کد پیگیری": order.tracking_code, "مرحله": stage.label, "ورودی": extra_input_value, } return _FIELD_PATTERN.sub(lambda m: values.get(m.group(1), m.group(0)), body) @staticmethod def _upsert_contact(user: User, order: Order) -> Contact: contact = Contact.objects.filter(user=user, phone=order.customer_phone).first() if contact: return contact return Contact.objects.create( id=str(uuid.uuid4()), user=user, name=order.customer_name, phone=order.customer_phone ) @staticmethod @transaction.atomic def advance(user: User, order_id: str, extra_input_value: str = "") -> tuple[Order, bool]: """برمی‌گرداند: (سفارشِ به‌روزشده، آیا پیامکِ این مرحله واقعا ارسال شد). قبلا موفقیتِ ثبتِ مرحله را با موفقیتِ ارسالِ پیامک قاطی می‌کرد؛ الان اگر قالب متن نداشته باشد یا کاوه‌نگار ارسال را رد کند، مرحله همچنان ثبت می‌شود ولی caller می‌تواند به کاربر بگوید که پیامک نرفت (مثلا چون SENDER_LINE در تنظیمات سرور خالی است).""" order = Order.objects.select_for_update().get(pk=order_id, user=user) if order.is_completed: raise ValueError("این سفارش قبلا به آخرین مرحله رسیده است.") stages = list(OrderStage.objects.filter(user=user).order_by("sort_order")) if not stages: raise ValueError("اول باید حداقل یک مرحله برای پیگیری سفارش تعریف کنی.") if order.current_stage_id is None: next_stage = stages[0] else: current_index = next((i for i, s in enumerate(stages) if s.id == order.current_stage_id), None) if current_index is None or current_index + 1 >= len(stages): raise ValueError("این سفارش قبلا به آخرین مرحله رسیده است.") next_stage = stages[current_index + 1] extra_input_value = (extra_input_value or "").strip() if next_stage.requires_extra_input and not extra_input_value: label = next_stage.extra_input_label or "ورودی این مرحله" raise ValueError(f"برای این مرحله باید «{label}» را وارد کنی.") contact = OrderStageService._upsert_contact(user, order) rendered = OrderStageService._render(order, next_stage, extra_input_value) message_sent = False if rendered: bulk = BulkMessageService.create_and_dispatch( user=user, channels=[Channel.SMS], contact_ids=[contact.id], body_override=rendered, ) message_sent = bulk.recipients.filter( channel=Channel.SMS, status=MessageRecipient.Status.SENT ).exists() order.current_stage = next_stage order.is_completed = next_stage.id == stages[-1].id order.save(update_fields=["current_stage", "is_completed", "updated_at"]) OrderStageLog.objects.create( id=str(uuid.uuid4()), order=order, stage=next_stage, stage_label=next_stage.label, message_body=rendered, extra_input_value=extra_input_value, ) return order, message_sent