Features: 1) Integrated CRM trigger functionality for Order model, allowing CRM updates during state changes; 2) Added handling for multiple or missing OrderCrmLink entries within upsert_order; 3) Introduced fns_api_key handling in CRM configurations.
Fixes: 1) Resolved malformed phone number issue in serializer when `instance` is unavailable; 2) Corrected relation field naming for `OrderCrmLink` (`order_uuid` to `order`); 3) Ensured only one default CRM provider can exist. Extra: 1) Refactored `is_business` logic with error handling; 2) Improved logging for CRM integration errors; 3) Added `integration_location` and `default` fields to CRM configuration model.
This commit is contained in:
parent
752f96fcdd
commit
d203095c65
4 changed files with 61 additions and 11 deletions
|
|
@ -12,13 +12,13 @@ from core.models import CustomerRelationshipManagementProvider, Order, OrderCrmL
|
||||||
logger = logging.getLogger("django")
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
|
||||||
class AmoOrderGateway:
|
class AmoCRM:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
try:
|
try:
|
||||||
self.instance = CustomerRelationshipManagementProvider.objects.get(name="amo")
|
self.instance = CustomerRelationshipManagementProvider.objects.get(name="amo")
|
||||||
except CustomerRelationshipManagementProvider.DoesNotExist:
|
except CustomerRelationshipManagementProvider.DoesNotExist as dne:
|
||||||
logger.warning("AMO CRM provider not found")
|
logger.warning("AMO CRM provider not found")
|
||||||
raise CRMException("AMO CRM provider not found")
|
raise CRMException("AMO CRM provider not found") from dne
|
||||||
|
|
||||||
self.base = f"https://{self.instance.integration_url}"
|
self.base = f"https://{self.instance.integration_url}"
|
||||||
|
|
||||||
|
|
@ -31,6 +31,8 @@ class AmoOrderGateway:
|
||||||
self.stage_map = self.instance.attributes.get("stage_map")
|
self.stage_map = self.instance.attributes.get("stage_map")
|
||||||
self.responsible_user_id = self.instance.attributes.get("responsible_user_id")
|
self.responsible_user_id = self.instance.attributes.get("responsible_user_id")
|
||||||
|
|
||||||
|
self.fns_api_key = self.instance.attributes.get("fns_api_key")
|
||||||
|
|
||||||
if not all(
|
if not all(
|
||||||
[
|
[
|
||||||
self.base,
|
self.base,
|
||||||
|
|
@ -39,6 +41,7 @@ class AmoOrderGateway:
|
||||||
self.redirect_uri,
|
self.redirect_uri,
|
||||||
self.pipeline_id,
|
self.pipeline_id,
|
||||||
self.stage_map,
|
self.stage_map,
|
||||||
|
self.fns_api_key,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
raise CRMException("AMO CRM provider not configured")
|
raise CRMException("AMO CRM provider not configured")
|
||||||
|
|
@ -79,7 +82,12 @@ class AmoOrderGateway:
|
||||||
|
|
||||||
def upsert_order(self, order: Order) -> str:
|
def upsert_order(self, order: Order) -> str:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
link: Optional[OrderCrmLink] = OrderCrmLink.objects.filter(order=order).first()
|
try:
|
||||||
|
link: Optional[OrderCrmLink] = OrderCrmLink.objects.get(order=order)
|
||||||
|
except OrderCrmLink.MultipleObjectsReturned:
|
||||||
|
link = OrderCrmLink.objects.filter(order=order).first()
|
||||||
|
except OrderCrmLink.DoesNotExist:
|
||||||
|
link = None
|
||||||
if link:
|
if link:
|
||||||
lead_id = link.crm_lead_id
|
lead_id = link.crm_lead_id
|
||||||
payload = self._build_lead_payload(order)
|
payload = self._build_lead_payload(order)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
|
from contextlib import suppress
|
||||||
from typing import Any, Optional, Self
|
from typing import Any, Optional, Self
|
||||||
|
|
||||||
from constance import config
|
from constance import config
|
||||||
|
|
@ -57,6 +59,7 @@ from core.utils.db import TweakedAutoSlugField, unicode_slugify_function
|
||||||
from core.utils.lists import FAILED_STATUSES
|
from core.utils.lists import FAILED_STATUSES
|
||||||
from core.validators import validate_category_image_dimensions
|
from core.validators import validate_category_image_dimensions
|
||||||
from evibes.settings import CURRENCY_CODE
|
from evibes.settings import CURRENCY_CODE
|
||||||
|
from evibes.utils.misc import create_object
|
||||||
from payments.models import Transaction
|
from payments.models import Transaction
|
||||||
|
|
||||||
logger = logging.getLogger("django")
|
logger = logging.getLogger("django")
|
||||||
|
|
@ -1428,9 +1431,13 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_business(self) -> bool:
|
def is_business(self) -> bool:
|
||||||
return (self.attributes.get("is_business", False) if self.attributes else False) or (
|
with suppress(Exception):
|
||||||
self.user.attributes.get("is_business", False) if self.user else False
|
return (self.attributes.get("is_business", False) if self.attributes else False) or (
|
||||||
)
|
(self.user.attributes.get("is_business", False) and self.user.attributes.get("business_identificator"))
|
||||||
|
if self.user
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
def save(self, **kwargs) -> Self:
|
def save(self, **kwargs) -> Self:
|
||||||
pending_orders = 0
|
pending_orders = 0
|
||||||
|
|
@ -1776,6 +1783,29 @@ class Order(ExportModelOperationsMixin("order"), NiceModel): # type: ignore [mi
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def trigger_crm(self):
|
||||||
|
crm_links = OrderCrmLink.objects.filter(order=self)
|
||||||
|
if crm_links.exists():
|
||||||
|
crm_link = crm_links.first()
|
||||||
|
crm_integration = create_object(crm_link.crm.integration_location, crm_link.crm.name)
|
||||||
|
try:
|
||||||
|
crm_integration.upsert_order(self)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed to trigger crm integration {crm_link.crm.name} for order {self.uuid}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
crm = CustomerRelationshipManagementProvider.objects.get(default=True)
|
||||||
|
crm_integration = create_object(crm.integration_location, crm.name)
|
||||||
|
try:
|
||||||
|
crm_integration.upsert_order(self)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed to trigger crm integration {crm.name} for order {self.uuid}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # type: ignore [misc, django-manager-missing]
|
class OrderProduct(ExportModelOperationsMixin("order_product"), NiceModel): # type: ignore [misc, django-manager-missing]
|
||||||
"""
|
"""
|
||||||
|
|
@ -1935,17 +1965,24 @@ class CustomerRelationshipManagementProvider(ExportModelOperationsMixin("crm_pro
|
||||||
integration_url = URLField(blank=True, null=True, help_text=_("URL of the integration"))
|
integration_url = URLField(blank=True, null=True, help_text=_("URL of the integration"))
|
||||||
authentication = JSONField(blank=True, null=True, help_text=_("authentication credentials"))
|
authentication = JSONField(blank=True, null=True, help_text=_("authentication credentials"))
|
||||||
attributes = JSONField(blank=True, null=True, help_name=_("attributes"))
|
attributes = JSONField(blank=True, null=True, help_name=_("attributes"))
|
||||||
|
integration_location = CharField(max_length=128, blank=True, null=True)
|
||||||
|
default = BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.crm_lead_id
|
return self.crm_lead_id
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if self.objects.filter(default=True).exists():
|
||||||
|
raise ValueError(_("you can only have one default CRM provider"))
|
||||||
|
super().save(**kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("order CRM link")
|
verbose_name = _("order CRM link")
|
||||||
verbose_name_plural = _("orders CRM links")
|
verbose_name_plural = _("orders CRM links")
|
||||||
|
|
||||||
|
|
||||||
class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel):
|
class OrderCrmLink(ExportModelOperationsMixin("order_crm_link"), NiceModel):
|
||||||
order_uuid = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links")
|
order = ForeignKey(to=Order, on_delete=PROTECT, related_name="crm_links")
|
||||||
crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links")
|
crm = ForeignKey(to=CustomerRelationshipManagementProvider, on_delete=PROTECT, related_name="order_links")
|
||||||
crm_lead_id = CharField(max_length=30, unique=True, db_index=True)
|
crm_lead_id = CharField(max_length=30, unique=True, db_index=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
from core.crm import any_crm_integrations
|
||||||
from core.models import Category, Order, Product, PromoCode, Wishlist, DigitalAssetDownload
|
from core.models import Category, Order, Product, PromoCode, Wishlist, DigitalAssetDownload
|
||||||
from core.utils import (
|
from core.utils import (
|
||||||
generate_human_readable_id,
|
generate_human_readable_id,
|
||||||
|
|
@ -67,6 +68,9 @@ def process_order_changes(instance, created, **_kwargs):
|
||||||
if type(instance.attributes) is not dict:
|
if type(instance.attributes) is not dict:
|
||||||
instance.attributes = {}
|
instance.attributes = {}
|
||||||
|
|
||||||
|
if any_crm_integrations():
|
||||||
|
instance.trigger_crm()
|
||||||
|
|
||||||
if not created:
|
if not created:
|
||||||
if instance.status != "PENDING" and instance.user:
|
if instance.status != "PENDING" and instance.user:
|
||||||
pending_orders = Order.objects.filter(user=instance.user, status="PENDING")
|
pending_orders = Order.objects.filter(user=instance.user, status="PENDING")
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,10 @@ class UserSerializer(ModelSerializer):
|
||||||
raise ValidationError(_("passwords do not match"))
|
raise ValidationError(_("passwords do not match"))
|
||||||
if "phone_number" in attrs:
|
if "phone_number" in attrs:
|
||||||
validate_phone_number(attrs["phone_number"])
|
validate_phone_number(attrs["phone_number"])
|
||||||
if User.objects.filter(phone_number=attrs["phone_number"]).exclude(uuid=self.instance.uuid).exists():
|
if self.instance:
|
||||||
phone_number = attrs["phone_number"]
|
if User.objects.filter(phone_number=attrs["phone_number"]).exclude(uuid=self.instance.uuid).exists():
|
||||||
raise ValidationError(_(f"malformed phone number: {phone_number}"))
|
phone_number = attrs["phone_number"]
|
||||||
|
raise ValidationError(_(f"malformed phone number: {phone_number}"))
|
||||||
if "email" in attrs:
|
if "email" in attrs:
|
||||||
validate_email(attrs["email"])
|
validate_email(attrs["email"])
|
||||||
if User.objects.filter(email=attrs["email"]).exclude(uuid=self.instance.uuid).exists():
|
if User.objects.filter(email=attrs["email"]).exclude(uuid=self.instance.uuid).exists():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue