Skip to content

Commit aecacee

Browse files
committed
Backend: AutoClose Rule Initilaized
1 parent beccbbc commit aecacee

File tree

13 files changed

+238
-31
lines changed

13 files changed

+238
-31
lines changed

management/core/admin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@
1919
Written in 2025 by Dorna Raj Gyawali <dronarajgyawali@gmail.com>
2020
"""
2121

22+
from core.models import Agent, Customer, Department, Ticket, User
2223
from django.contrib import admin
2324
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
2425

25-
from core.models import Agent, Customer, Department, Ticket, User
26-
2726

2827
@admin.register(User)
2928
class UserAdmin(BaseUserAdmin):

management/core/api/viewset.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
import logging
1212
from datetime import datetime
1313

14+
from core import dumps, validators
15+
from core.constants import Status
16+
from core.models import Agent, Customer, Ticket
17+
from core.permissions import CanEditOwnOrAdmin
18+
from core.serializer import RegisterSerializer, TicketCreateSerializer
19+
from core.tasks import process_ticket_queue
1420
from django.db import transaction
1521
from django.shortcuts import get_object_or_404
1622
from django.utils import timezone
@@ -21,13 +27,6 @@
2127
from rest_framework.response import Response
2228
from rest_framework_simplejwt.tokens import RefreshToken
2329

24-
from core import dumps, validators
25-
from core.constants import Status
26-
from core.models import Agent, Customer, Ticket
27-
from core.permissions import CanEditOwnOrAdmin
28-
from core.serializer import RegisterSerializer, TicketCreateSerializer
29-
from core.tasks import process_ticket_queue
30-
3130
logger = logging.getLogger(__name__)
3231

3332

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
The AutoClose class, which is responsible for automatically closing
3+
inactive tickets after a specified period of inactivity. It extends the BaseRule class
4+
and provides methods to check if the rule should be applied and to apply the rule.
5+
6+
The AutoClose class ensures that tickets in a "WAITING" status for a defined number of
7+
days are closed automatically, helping to manage ticket lifecycles efficiently.
8+
9+
Copyright (c) Supportix. All rights reserved.
10+
Written in 2025 by Dorna Raj Gyawali <dronarajgyawali@gmail.com>
11+
"""
12+
13+
from datetime import timedelta
14+
15+
from core.automation.base_rule import BaseRule
16+
from core.constants import Status
17+
from core.models import AutoEscalate, Ticket
18+
from django.utils import timezone
19+
20+
21+
class AutoClose(BaseRule):
22+
def __init__(self, ticket_id, **kwargs):
23+
super().__init__(ticket_id, **kwargs)
24+
self.inactive_days = kwargs.get("inactive_days", 90)
25+
26+
def should_apply(self):
27+
try:
28+
ticket = Ticket.objects.get(ticket_id=self.ticket_id)
29+
except Ticket.DoesNotExist:
30+
return False
31+
32+
return (
33+
ticket.status == Status.WAITING
34+
and ticket.created_at < timezone.now() - timedelta(days=self.inactive_days)
35+
)
36+
37+
def apply(self):
38+
try:
39+
ticket = Ticket.objects.get(ticket_id=self.ticket_id)
40+
if not self.should_apply():
41+
return {
42+
"operation": "apply",
43+
"status": "failed",
44+
"reason": "condition doesnot meet",
45+
}
46+
kwargs = {
47+
"ticket_id": ticket,
48+
"new_status": Status.CLOSED,
49+
}
50+
if ticket.agent:
51+
kwargs["new_agent"] = ticket.agent
52+
53+
result = AutoEscalate.escalate_changes(**kwargs)
54+
return {
55+
"operation": "apply",
56+
"status": "success" if result["success"] else "failed",
57+
"details": result["message"],
58+
}
59+
except Ticket.DoesNotExist:
60+
return {
61+
"operation": "apply",
62+
"status": "failed",
63+
"reason": "Ticket not found",
64+
}
65+
except Exception as e:
66+
return {"operation": "apply", "status": "failed", "reason": str(e)}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
BaseRule is a base class for rules used for automation.
3+
4+
This abstract base class defines the structure and contract for automation rules
5+
that can be applied to tickets. Subclasses must implement the abstract methods
6+
to provide specific rule logic.
7+
8+
Copyright (c) Supportix. All rights reserved.
9+
Written in 2025 by Dorna Raj Gyawali <dronarajgyawali@gmail.com>
10+
"""
11+
12+
from abc import ABC, abstractmethod
13+
14+
15+
class BaseRule(ABC):
16+
"""
17+
BaseRule is an abstract base class that defines the structure for automation rules.
18+
19+
Attributes:
20+
ticket_id (Any): The identifier of the ticket to which the rule applies.
21+
params (dict): Additional parameters for the rule.
22+
23+
Methods:
24+
should_apply():
25+
Abstract method to determine if the rule should be applied.
26+
Must be implemented by subclasses.
27+
28+
apply():
29+
Abstract method to apply the rule logic.
30+
Must be implemented by subclasses.
31+
"""
32+
33+
def __init__(self, ticket, **kwargs):
34+
self.ticket_id = ticket
35+
self.params = kwargs
36+
37+
@abstractmethod
38+
def should_apply(self):
39+
"""
40+
Abstract method to determine if the rule should be applied.
41+
Must be implemented by subclasses.
42+
"""
43+
pass
44+
45+
@abstractmethod
46+
def apply(self):
47+
"""
48+
Abstract method to determine if the rule should be applied.
49+
Must be implemented by subclasses.
50+
"""
51+
pass
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
RuleEngine is a class responsible for managing and executing a set of rules on a given ticket.
3+
Attributes:
4+
ticket_id (int): The unique identifier of the ticket to which the rules will be applied.
5+
rules (list): A list of rule objects that define the logic to be executed.
6+
Methods:
7+
__init__(ticket_id):
8+
Initializes the RuleEngine with a ticket ID and a predefined set of rules.
9+
run():
10+
Executes all the rules in the `rules` list. For each rule, it checks if the rule should be applied
11+
using the `should_apply` method. If applicable, it applies the rule using the `apply` method and
12+
collects the results in a context list. Returns the context containing details of applied rules.
13+
14+
Copyright (c) Supportix. All rights reserved.
15+
Written in 2025 by Dorna Raj Gyawali <dronarajgyawali@gmail.com>
16+
"""
17+
18+
from core.automation.auto_close import AutoClose
19+
20+
21+
class RuleEngine:
22+
def __init__(self, ticket_id):
23+
self.ticket_id = ticket_id
24+
self.rules = [AutoClose(ticket_id, inactive_days=1)]
25+
26+
def run(self):
27+
context = []
28+
for rule in self.rules:
29+
if rule.should_apply():
30+
result = rule.apply()
31+
context.append({"rule": rule.__class__.__name__, "details": result})
32+
return context

management/core/models.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@
1818
Written in 2025 by Dorna Raj Gyawali <dronarajgyawali@gmail.com>
1919
"""
2020

21-
from datetime import timezone
22-
21+
from core.constants import Role, Status
22+
from core.dumps import CONTEXT_403
2323
from django.conf import settings
2424
from django.contrib.auth.models import AbstractUser
2525
from django.db import models
2626
from django.forms import model_to_dict
27-
28-
from core.constants import Role, Status
29-
from core.dumps import CONTEXT_403
27+
from django.utils import timezone
3028

3129

3230
class User(AbstractUser):
@@ -200,11 +198,12 @@ def escalate_changes(cls, ticket_id, new_status, new_agent=None):
200198
try:
201199
ticket = Ticket.objects.get(ticket_id=ticket_id)
202200
status_change = StatusChange.objects.create(
201+
ticket=ticket,
203202
new_status=new_status,
204203
new_agent=new_agent,
205204
new_queued_at=timezone.now(),
206205
)
207-
cls.objects.create(ticket=ticket, status_changes=status_change)
206+
cls.objects.create(ticket=ticket, status_change=status_change)
208207
ticket.status = new_status
209208
if new_agent:
210209
ticket.agent = new_agent

management/core/serializer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1+
from core.models import Agent, Customer, Department, Role, Ticket
12
from django.contrib.auth import get_user_model
23
from rest_framework import serializers
34

4-
from core.models import Agent, Customer, Department, Role, Ticket
5-
65
User = get_user_model()
76

87

management/core/tasks.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
from datetime import timedelta
22

33
from celery import shared_task
4-
from django.db import transaction
5-
from django.db.models import Q
6-
from django.utils import timezone
7-
84
from core.automation.state_machine import TicketStateMachine
95
from core.constants import Status
106
from core.models import Agent, Ticket
7+
from django.db import transaction
8+
from django.db.models import Q
9+
from django.utils import timezone
1110

1211

1312
@shared_task(bind=True)

management/core/tests/test_api.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@
99
from datetime import datetime, timedelta
1010
from unittest import mock
1111

12+
from core.constants import Status
13+
from core.models import Agent, Customer, Department, Ticket, User
1214
from django.db.models import F
1315
from django.test import TestCase
1416
from django.urls import reverse
1517
from django.utils.timezone import make_aware
1618
from rest_framework import status
1719
from rest_framework.test import APIClient
1820

19-
from core.constants import Status
20-
from core.models import Agent, Customer, Department, Ticket, User
21-
2221

2322
class ApiViewsTest(TestCase):
2423
"""Test cases for all API endpoints in a single class."""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from datetime import timedelta
2+
3+
from core.automation.auto_close import AutoClose
4+
from core.automation.rule_runner import RuleEngine
5+
from core.models import Agent, Customer, Department, Status, Ticket, User
6+
from django.test import TestCase
7+
from django.utils import timezone
8+
9+
10+
class AutoCloseRuleTestCase(TestCase):
11+
def setUp(self):
12+
self.department = Department.objects.create(name="Support")
13+
14+
# Create user with agent role
15+
self.agent_user = User.objects.create_user(
16+
username="testagent",
17+
email="testagent@example.com",
18+
password="testpassword123",
19+
role="agent",
20+
)
21+
self.agent = Agent.objects.create(
22+
user=self.agent_user,
23+
department=self.department,
24+
is_available=True,
25+
max_customers=5,
26+
current_customers=0,
27+
)
28+
29+
# Create user with customer role
30+
self.customer_user = User.objects.create_user(
31+
username="testcustomer",
32+
email="testcustomer@example.com",
33+
password="testpassword123",
34+
role="customer",
35+
)
36+
self.customer = Customer.objects.create(
37+
user=self.customer_user,
38+
is_paid=True,
39+
)
40+
41+
# Create ticket normally
42+
self.ticket = Ticket.objects.create(
43+
ticket_id="TID-TEST123",
44+
customer=self.customer,
45+
agent=self.agent,
46+
issue_title="Test issue for autoclose",
47+
issue_desc="Description",
48+
status=Status.WAITING,
49+
)
50+
Ticket.objects.filter(pk=self.ticket.pk).update(
51+
created_at=timezone.now() - timedelta(days=2)
52+
)
53+
self.ticket.refresh_from_db()
54+
55+
def test_autoclose_rule_escalates_ticket(self):
56+
rule = AutoClose(ticket_id=self.ticket.ticket_id, inactive_days=1)
57+
58+
self.assertTrue(rule.should_apply())
59+
60+
result = rule.apply()
61+
62+
self.ticket.refresh_from_db()
63+
64+
self.assertEqual(self.ticket.status, Status.CLOSED)
65+
66+
self.assertEqual(result["status"], "success")
67+
self.assertIn("escalated", result["details"].lower())

0 commit comments

Comments
 (0)