from django.db.models.signals import pre_save, post_delete from django.dispatch import receiver from django.core.mail import send_mail from django.conf import settings from django.urls import reverse from .models import Order, OrderItem, OrderStatus from django.db import transaction from django.contrib.auth.signals import user_logged_in from .cart import transfer_cart @receiver([pre_save, post_delete], sender=Order) def send_order_notification(sender, instance, **kwargs): """Send an email notification when an order is created, updated, or deleted.""" # For pre_save, we need to check if the instance exists in the database if kwargs.get('signal', None) == pre_save: try: # Get the current instance from the database old_instance = Order.objects.get(pk=instance.pk) # Only send notification if status has changed if old_instance.status != instance.status: # Execute on commit to ensure DB consistency transaction.on_commit(lambda: _send_order_email(instance, old_status=old_instance.status, **kwargs)) except Order.DoesNotExist: # This is a new instance (creation) # You might want to handle creation differently or just pass pass else: # Handle post_delete transaction.on_commit(lambda: _send_order_email(instance, **kwargs)) def _send_order_email(instance, old_status=None, **kwargs): # Skip processing for PENDING orders if instance.status == OrderStatus.PENDING: return # Determine action type if 'signal' in kwargs and kwargs['signal'] == post_delete: action = "DELETED" elif old_status is None: action = "CREATED" else: action = "UPDATED" if action in ["DELETED", "CREATED"]: return # No emails for these actions # Build common email components order_details = _get_order_details(instance) items_list = _build_items_list(instance.id, action) # Send internal notification _send_internal_notification(instance, action, order_details, items_list) # Send customer notification if applicable if order_details['customer_email']: _send_customer_notification(instance, order_details, items_list) def _get_order_details(instance): """Extract and build order details dictionary.""" # Get customer info customer_email = None if instance.user: customer_info = f"Utilisateur: {instance.user.email}" customer_email = instance.user.email elif instance.guest_user: customer_info = f"Invité: {instance.guest_user.email} ({instance.guest_user.phone})" customer_email = instance.guest_user.email else: customer_info = "Client inconnu" # Translate statuses status_fr_map = { "PENDING": "EN ATTENTE", "PAID": "PAYÉE", "SHIPPED": "EXPÉDIÉE", "DELIVERED": "LIVRÉE", "CANCELED": "ANNULÉE", "REFUNDED": "REMBOURSÉE", "PREPARED": "EN COURS DE PRÉPARATION", "READY": "PRÊT" } payment_status_fr_map = { "UNPAID": "NON PAYÉE", "PAID": "PAYÉE", "FAILED": "ÉCHOUÉE", "REFUNDED": "REMBOURSÉE", } # Calculate discount information has_coupon = instance.coupon is not None coupon_info = "" final_price = instance.total_price if has_coupon: coupon_code = instance.coupon.code discount_amount = instance.discount_amount final_price = instance.get_total_after_discount() if instance.coupon.discount_percent > 0: coupon_info = f"Code promo: {coupon_code} ({instance.coupon.discount_percent}%)" else: coupon_info = f"Code promo: {coupon_code} (€{discount_amount})" return { 'order_id': instance.id, 'status': instance.status, 'status_fr': status_fr_map.get(instance.status, instance.status), 'payment_status': instance.payment_status, 'payment_status_fr': payment_status_fr_map.get(instance.payment_status, instance.payment_status), 'total_price': instance.total_price, 'has_coupon': has_coupon, 'coupon_info': coupon_info, 'discount_amount': instance.discount_amount if has_coupon else 0, 'final_price': final_price, 'customer_info': customer_info, 'customer_email': customer_email, 'date_ordered': instance.date_ordered, 'admin_url': f"{settings.SHOP_SITE_ROOT_URL}{reverse('admin:shop_order_change', args=[instance.id])}", 'shipping_address': instance.get_shipping_address(), } def _build_items_list(order_id, action): """Build the list of order items.""" items_list = "" if action != "DELETED": order_items = OrderItem.objects.filter(order_id=order_id).select_related('product', 'color', 'size') for item in order_items: color = item.color.name if item.color else "N/A" size = item.size.name if item.size else "N/A" items_list += f"- {item.quantity}x {item.product.title} (Couleur: {color}, Taille: {size}, Prix: {item.price}€)\n" return items_list def _translate_action(action): """Translate action to French.""" translations = { "CREATED": "CRÉÉE", "UPDATED": "MISE À JOUR", "DELETED": "SUPPRIMÉE" } return translations.get(action, action) def _send_internal_notification(instance, action, order_details, items_list): """Send notification email to shop managers.""" action_fr = _translate_action(action) # Build price information with coupon details if applicable price_info = f"Prix total: {order_details['total_price']}€" server = "" if settings.DEBUG: server = "DEBUG: " if order_details['has_coupon']: price_info = f""" Prix total: {order_details['total_price']}€ {order_details['coupon_info']} Réduction: -{order_details['discount_amount']}€ Montant payé: {order_details['final_price']}€""" subject = f"{server}Commande #{order_details['order_id']} {action_fr}: {order_details['status_fr']}" message = f""" La commande #{order_details['order_id']} a été {action_fr.lower()} Statut: {order_details['status_fr']} Statut de paiement: {order_details['payment_status_fr']} {price_info} {order_details['customer_info']} {order_details['shipping_address']} Articles: {items_list} Voir la commande dans le panneau d'administration: {order_details['admin_url']} Ceci est un message automatique. Merci de ne pas répondre. """ # Send internal email recipient_list = [email for name, email in settings.SHOP_MANAGERS] if not recipient_list: recipient_list = [settings.DEFAULT_FROM_EMAIL] send_mail( subject=subject, message=message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=recipient_list, fail_silently=False, ) def _send_customer_notification(instance, order_details, items_list): """Send appropriate notification email to customer based on order status.""" # Common email variables contact_email = settings.SHOP_SUPPORT_EMAIL shop_url = f"{settings.SHOP_SITE_ROOT_URL}/shop" date_formatted = order_details['date_ordered'].strftime('%d/%m/%Y') # Determine email content based on status and payment status email_content = _get_customer_email_content( instance.status, order_details['payment_status'], order_details['order_id'], date_formatted, order_details['status_fr'], order_details['total_price'], order_details['has_coupon'], order_details['coupon_info'], order_details['discount_amount'], order_details['final_price'], items_list, contact_email, shop_url, order_details['shipping_address'] ) # Skip if no email content returned if not email_content: return # Send email to customer send_mail( subject=email_content['subject'], message=email_content['message'], from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[order_details['customer_email']], fail_silently=False, ) def _get_customer_email_content(status, payment_status, order_id, date, status_fr, total_price, has_coupon, coupon_info, discount_amount, final_price, items_list, contact_email, shop_url, shipping_address): """Get the appropriate customer email content based on order status.""" # Build price information with coupon details if applicable price_info = f"Prix total: {total_price}€" if has_coupon: price_info = f"""Prix total: {total_price}€ {coupon_info} Réduction: -{discount_amount}€ Montant payé: {final_price}€""" # Payment confirmation email if status == OrderStatus.PAID and payment_status == "PAID": return { 'subject': f"Confirmation de votre commande #{order_id} - Padel Club", 'message': _build_payment_confirmation_email(order_id, date, status_fr, price_info, items_list, contact_email, shop_url, shipping_address) } # Order status update email elif status in [OrderStatus.SHIPPED, OrderStatus.DELIVERED, OrderStatus.CANCELED, OrderStatus.PREPARED, OrderStatus.REFUNDED, OrderStatus.READY]: status_message = { OrderStatus.PREPARED: "Votre commande est en cours de préparation.", OrderStatus.READY: "Votre commande est prête.", OrderStatus.SHIPPED: "Votre commande a été expédiée et est en cours de livraison.", OrderStatus.DELIVERED: "Votre commande a été livrée. Nous espérons que vous apprécierez vos produits !", OrderStatus.REFUNDED: "Votre commande a été annulée et son remboursement est en cours de traitement.", OrderStatus.CANCELED: "Votre commande a été annulée. Si vous n'êtes pas à l'origine de cette annulation, veuillez nous contacter immédiatement." }.get(status, "") return { 'subject': f"Mise à jour de votre commande #{order_id} - Padel Club", 'message': _build_status_update_email(order_id, date, status_message, status_fr, price_info, items_list, contact_email, shipping_address) } # Payment issue notification elif payment_status == "FAILED": return { 'subject': f"Problème de paiement pour votre commande #{order_id} - Padel Club", 'message': _build_payment_issue_email(order_id, date, price_info, items_list, contact_email, shop_url) } # Payment reminder for unpaid orders elif payment_status == "UNPAID" and status != OrderStatus.PENDING: return { 'subject': f"Rappel de paiement pour votre commande #{order_id} - Padel Club", 'message': _build_payment_reminder_email(order_id, date, price_info, items_list, contact_email) } # No email needed return None def _build_payment_confirmation_email(order_id, date, status_fr, price_info, items_list, contact_email, shop_url, shipping_address): """Build payment confirmation email message.""" return f""" Bonjour, Nous vous remercions pour votre commande sur Padel Club ! Récapitulatif de votre commande #{order_id} du {date} : Statut: {status_fr} {price_info} Détail de votre commande : {items_list} IMPORTANT - COMMENT RÉCUPÉRER VOTRE COMMANDE : Notre boutique fonctionne entre amis 'Padel Club'. Nous allons préparer votre commande et vous la remettre en main propre lors d'une prochaine session de padel ! Si jamais la livraison est possible, nous vous expédierons votre commande à l'adresse indiquée. Vous serez alors notifiés par email lorsque votre commande sera expédiée. {shipping_address} Pour toute question concernant votre commande, n'hésitez pas à contacter notre service client : {contact_email} Visitez notre boutique pour découvrir d'autres produits : {shop_url} Merci de votre confiance et à bientôt sur Padel Club ! L'équipe Padel Club """ def _build_status_update_email(order_id, date, status_message, status_fr, price_info, items_list, contact_email, shipping_address): """Build status update email message.""" return f""" Bonjour, Mise à jour concernant votre commande Padel Club #{order_id} du {date} : {status_message} {shipping_address} Statut actuel: {status_fr} {price_info} Détail de votre commande : {items_list} Pour toute question concernant votre commande, n'hésitez pas à contacter notre service client : {contact_email} Merci de votre confiance et à bientôt sur Padel Club ! L'équipe Padel Club """ def _build_payment_issue_email(order_id, date, price_info, items_list, contact_email, shop_url): """Build payment issue email message.""" return f""" Bonjour, Nous avons rencontré un problème lors du traitement du paiement de votre commande Padel Club #{order_id}. Détails de la commande : Date: {date} {price_info} Articles: {items_list} Veuillez vérifier vos informations de paiement et réessayer. Si le problème persiste, n'hésitez pas à contacter notre service client : {contact_email} Vous pouvez également visiter notre boutique pour finaliser votre achat : {shop_url} Merci de votre compréhension. L'équipe Padel Club """ def _build_payment_reminder_email(order_id, date, price_info, items_list, contact_email): """Build payment reminder email message.""" return f""" Bonjour, Nous vous rappelons que votre commande Padel Club #{order_id} du {date} n'a pas encore été payée. Détails de la commande : {price_info} Articles: {items_list} Pour finaliser votre commande, veuillez procéder au paiement dès que possible. Si vous rencontrez des difficultés ou si vous avez des questions, n'hésitez pas à contacter notre service client : {contact_email} Merci de votre confiance. L'équipe Padel Club """ @receiver(user_logged_in) def user_logged_in_handler(sender, request, user, **kwargs): """ When a user logs in, transfer any cart items from their anonymous session """ # Get the anonymous session key if hasattr(request, 'session') and not request.session.is_empty(): anonymous_session_key = request.session.session_key # After the user logs in, the session key changes # So we transfer cart from the old session to the new session if anonymous_session_key: transfer_cart(request, anonymous_session_key)