Skip to content

Commit 0cf597c

Browse files
committed
Reformat more tests in models
1 parent 68cf666 commit 0cf597c

11 files changed

+658
-24
lines changed

appointment/models.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import random
1111
import string
1212
import uuid
13+
from decimal import Decimal, InvalidOperation
1314

1415
from babel.numbers import get_currency_symbol
1516
from django.conf import settings
@@ -435,7 +436,7 @@ class Appointment(models.Model):
435436
want_reminder = models.BooleanField(default=False)
436437
additional_info = models.TextField(blank=True, null=True)
437438
paid = models.BooleanField(default=False)
438-
amount_to_pay = models.DecimalField(max_digits=6, decimal_places=2, blank=True, null=True)
439+
amount_to_pay = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
439440
id_request = models.CharField(max_length=100, blank=True, null=True)
440441

441442
# meta datas
@@ -453,16 +454,30 @@ def save(self, *args, **kwargs):
453454

454455
if self.id_request is None:
455456
self.id_request = f"{get_timestamp()}{self.appointment_request.id}{generate_random_id()}"
456-
if self.amount_to_pay is None or self.amount_to_pay == 0:
457-
payment_type = self.appointment_request.payment_type
458-
if payment_type == 'full':
459-
self.amount_to_pay = self.appointment_request.get_service_price()
460-
elif payment_type == 'down':
461-
self.amount_to_pay = self.appointment_request.get_service_down_payment()
462-
else:
463-
self.amount_to_pay = 0
457+
458+
try:
459+
# Ensure `amount_to_pay` is a Decimal and handle both int and float inputs
460+
if self.amount_to_pay is None:
461+
self.amount_to_pay = self._calculate_amount_to_pay()
462+
463+
self.amount_to_pay = self._to_decimal(self.amount_to_pay)
464+
except InvalidOperation:
465+
raise ValidationError("Invalid amount format for payment.")
466+
464467
return super().save(*args, **kwargs)
465468

469+
def _calculate_amount_to_pay(self):
470+
payment_type = self.appointment_request.payment_type
471+
if payment_type == 'full':
472+
return self.appointment_request.get_service_price()
473+
elif payment_type == 'down':
474+
return self.appointment_request.get_service_down_payment()
475+
else:
476+
return Decimal('0.00')
477+
478+
def _to_decimal(self, value):
479+
return Decimal(f"{value}").quantize(Decimal('0.01'))
480+
466481
def get_client_name(self):
467482
if hasattr(self.client, 'get_full_name') and callable(getattr(self.client, 'get_full_name')):
468483
name = self.client.get_full_name()
@@ -664,6 +679,10 @@ def clean(self):
664679
if self.lead_time is not None and self.finish_time is not None:
665680
if self.lead_time >= self.finish_time:
666681
raise ValidationError(_("Lead time must be before finish time"))
682+
if self.appointment_buffer_time is not None and self.appointment_buffer_time < 0:
683+
raise ValidationError(_("Appointment buffer time cannot be negative"))
684+
if self.slot_duration is not None and self.slot_duration <= 0:
685+
raise ValidationError(_("Slot duration must be greater than 0"))
667686

668687
def save(self, *args, **kwargs):
669688
self.clean()

appointment/tests/base/base_test.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ def clean_all_data(cls):
5454
Service.objects.all().delete()
5555
get_user_model().objects.all().delete()
5656

57-
def create_appt_request_for_sm1(self, **kwargs):
57+
def create_appt_request_for_sm1(self, service=None, staff_member=None, **kwargs):
5858
"""Create an appointment request for staff_member1."""
59-
return self.create_appointment_request_(service=self.service1, staff_member=self.staff_member1, **kwargs)
59+
service = service or self.service1
60+
staff_member = staff_member or self.staff_member1
61+
return self.create_appointment_request_(service=service, staff_member=staff_member, **kwargs)
6062

61-
def create_appt_request_for_sm2(self, **kwargs):
63+
def create_appt_request_for_sm2(self, service=None, staff_member=None, **kwargs):
6264
"""Create an appointment request for staff_member2."""
63-
return self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, **kwargs)
65+
service = service or self.service2
66+
staff_member = staff_member or self.staff_member2
67+
return self.create_appointment_request_(service=service, staff_member=staff_member, **kwargs)
6468

6569
def create_appt_for_sm1(self, appointment_request=None):
6670
if not appointment_request:

appointment/tests/mixins/base_mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def __init__(self):
7272
pass
7373

7474
@classmethod
75-
def create_appointment_(cls, user, appointment_request, phone="1234567890",
75+
def create_appointment_(cls, user, appointment_request, phone="+12392340543",
7676
address="Stargate Command, Cheyenne Mountain Complex, Colorado Springs, CO"):
7777
return Appointment.objects.create(
7878
client=user,
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from copy import deepcopy
2+
from datetime import date, datetime, time, timedelta
3+
4+
from django.core.exceptions import ValidationError
5+
from django.utils import timezone
6+
7+
from appointment.tests.base.base_test import BaseTest
8+
9+
10+
class AppointmentRequestCreationAndBasicAttributesTests(BaseTest):
11+
@classmethod
12+
def setUpTestData(cls):
13+
return super().setUpTestData()
14+
15+
@classmethod
16+
def tearDownClass(cls):
17+
return super().tearDownClass()
18+
19+
def setUp(self) -> None:
20+
self.ar = self.create_appt_request_for_sm1()
21+
return super().setUp()
22+
23+
def tearDown(self):
24+
self.ar.delete()
25+
super().tearDown()
26+
27+
def test_appointment_request_is_properly_created(self):
28+
self.assertIsNotNone(self.ar)
29+
self.assertEqual(self.ar.service, self.service1)
30+
self.assertEqual(self.ar.staff_member, self.staff_member1)
31+
self.assertEqual(self.ar.start_time, time(9, 0))
32+
self.assertEqual(self.ar.end_time, time(10, 0))
33+
self.assertIsNotNone(self.ar.get_id_request())
34+
self.assertEqual(self.ar.date, timezone.now().date())
35+
self.assertTrue(isinstance(self.ar.get_id_request(), str))
36+
self.assertIsNotNone(self.ar.created_at)
37+
self.assertIsNotNone(self.ar.updated_at)
38+
39+
def test_appointment_request_initial_state(self):
40+
"""Check the initial state of "reschedule attempts" and string representation."""
41+
self.assertEqual(self.ar.reschedule_attempts, 0)
42+
expected_representation = f"{self.ar.date} - {self.ar.start_time} to {self.ar.end_time} - {self.ar.service.name}"
43+
self.assertEqual(str(self.ar), expected_representation)
44+
45+
46+
class AppointmentRequestServiceAttributesTests(BaseTest):
47+
@classmethod
48+
def setUpTestData(cls):
49+
return super().setUpTestData()
50+
51+
@classmethod
52+
def tearDownClass(cls):
53+
return super().tearDownClass()
54+
55+
def setUp(self) -> None:
56+
self.ar = self.create_appt_request_for_sm1()
57+
return super().setUp()
58+
59+
def tearDown(self):
60+
self.ar.delete()
61+
super().tearDown()
62+
63+
def test_service_related_attributes_are_correct(self):
64+
"""Validate attributes related to the service within an appointment request."""
65+
self.assertEqual(self.ar.get_service_name(), self.service1.name)
66+
self.assertEqual(self.ar.get_service_price(), self.service1.get_price())
67+
self.assertEqual(self.ar.get_service_down_payment(), self.service1.get_down_payment())
68+
self.assertEqual(self.ar.get_service_image(), self.service1.image)
69+
self.assertEqual(self.ar.get_service_image_url(), self.service1.get_image_url())
70+
self.assertEqual(self.ar.get_service_description(), self.service1.description)
71+
self.assertTrue(self.ar.is_a_paid_service())
72+
self.assertEqual(self.ar.payment_type, 'full')
73+
self.assertFalse(self.ar.accepts_down_payment())
74+
75+
76+
class AppointmentRequestAttributeValidation(BaseTest):
77+
@classmethod
78+
def setUpTestData(cls):
79+
return super().setUpTestData()
80+
81+
@classmethod
82+
def tearDownClass(cls):
83+
return super().tearDownClass()
84+
85+
def setUp(self) -> None:
86+
self.ar = self.create_appt_request_for_sm1()
87+
return super().setUp()
88+
89+
def tearDown(self):
90+
self.ar.delete()
91+
super().tearDown()
92+
93+
def test_appointment_request_time_validations(self):
94+
"""Ensure start and end times are validated correctly."""
95+
ar = deepcopy(self.ar)
96+
97+
# End time before start time
98+
ar.start_time = time(11, 0)
99+
ar.end_time = time(9, 0)
100+
with self.assertRaises(ValidationError):
101+
ar.full_clean()
102+
103+
# End time equal to start time
104+
ar.end_time = time(11, 0)
105+
with self.assertRaises(ValidationError):
106+
ar.full_clean()
107+
108+
with self.assertRaises(ValidationError, msg="Start time and end time cannot be the same"):
109+
self.create_appointment_request_(
110+
self.service1, self.staff_member1, date_=date.today(), start_time=time(10, 0), end_time=time(10, 0)
111+
)
112+
113+
def test_appointment_request_date_validations(self):
114+
"""Validate that appointment requests cannot be in the past or have invalid durations."""
115+
ar = deepcopy(self.ar)
116+
117+
past_date = date.today() - timedelta(days=30)
118+
ar.date = past_date
119+
with self.assertRaises(ValidationError):
120+
ar.full_clean()
121+
122+
with self.assertRaises(ValidationError, msg="Date cannot be in the past"):
123+
self.create_appointment_request_(self.service1, self.staff_member1, date_=past_date)
124+
125+
with self.assertRaises(ValidationError, msg="The date is not valid"):
126+
date_ = datetime.strptime("31-03-2021", "%d-%m-%Y").date()
127+
self.create_appointment_request_(
128+
self.service1, self.staff_member1, date_=date_)
129+
130+
def test_appointment_duration_exceeds_service_time(self):
131+
"""Test that an appointment cannot be created with a duration greater than the service duration."""
132+
long_duration = timedelta(hours=3)
133+
service = self.create_service_(name="Asgard Technology Retrofit", duration=long_duration)
134+
service.duration = long_duration
135+
service.save()
136+
137+
# Create an appointment request with a 4-hour duration and the 3-hour service (should not work)
138+
with self.assertRaises(ValidationError):
139+
self.create_appointment_request_(service, self.staff_member1, start_time=time(9, 0),
140+
end_time=time(13, 0))
141+
142+
def test_invalid_payment_type_raises_error(self):
143+
"""Payment type must be either 'full' or 'down'"""
144+
ar = deepcopy(self.ar)
145+
ar.payment_type = "Naquadah Instead of Credits"
146+
with self.assertRaises(ValidationError):
147+
ar.full_clean()
148+
149+
150+
class AppointmentRequestRescheduleHistory(BaseTest):
151+
@classmethod
152+
def setUpTestData(cls):
153+
return super().setUpTestData()
154+
155+
@classmethod
156+
def tearDownClass(cls):
157+
return super().tearDownClass()
158+
159+
def setUp(self) -> None:
160+
service = deepcopy(self.service1)
161+
service.reschedule_limit = 2
162+
service.allow_rescheduling = True
163+
service.save()
164+
self.ar_ = self.create_appt_request_for_sm1(service=service)
165+
return super().setUp()
166+
167+
def test_ar_can_be_reschedule(self):
168+
self.assertTrue(self.ar_.can_be_rescheduled())
169+
170+
def test_reschedule_attempts_increment(self):
171+
self.assertTrue(self.ar_.can_be_rescheduled())
172+
self.ar_.increment_reschedule_attempts()
173+
self.assertEqual(self.ar_.reschedule_attempts, 1)
174+
self.assertTrue(self.ar_.can_be_rescheduled())
175+
self.ar_.increment_reschedule_attempts()
176+
self.assertEqual(self.ar_.reschedule_attempts, 2)
177+
self.assertFalse(self.ar_.can_be_rescheduled())
178+
179+
def test_no_reschedule_history(self):
180+
service = deepcopy(self.service1)
181+
ar = self.create_appointment_request_(service, self.staff_member1)
182+
self.assertFalse(ar.get_reschedule_history().exists())
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from datetime import timedelta
2+
3+
from django.core.exceptions import ValidationError
4+
from django.utils import timezone
5+
6+
from appointment.models import AppointmentRescheduleHistory
7+
from appointment.tests.base.base_test import BaseTest
8+
9+
10+
class AppointmentRescheduleHistoryCreationTests(BaseTest):
11+
@classmethod
12+
def setUpTestData(cls):
13+
super().setUpTestData()
14+
15+
def setUp(self):
16+
self.appointment_request = self.create_appt_request_for_sm1()
17+
self.future_date = timezone.now().date() + timedelta(days=3)
18+
return super().setUp()
19+
20+
def test_reschedule_history_creation_with_valid_data(self):
21+
reschedule_history = AppointmentRescheduleHistory.objects.create(
22+
appointment_request=self.appointment_request,
23+
date=self.future_date,
24+
start_time=timezone.now().time(),
25+
end_time=(timezone.now() + timedelta(hours=1)).time(),
26+
staff_member=self.staff_member1,
27+
reason_for_rescheduling="Client request",
28+
reschedule_status='pending'
29+
)
30+
self.assertIsNotNone(reschedule_history)
31+
self.assertEqual(reschedule_history.reschedule_status, 'pending')
32+
self.assertTrue(reschedule_history.still_valid())
33+
34+
def test_auto_generation_of_id_request_on_creation(self):
35+
reschedule_history = AppointmentRescheduleHistory.objects.create(
36+
appointment_request=self.appointment_request,
37+
date=self.future_date,
38+
start_time=timezone.now().time(),
39+
end_time=(timezone.now() + timedelta(hours=1)).time(),
40+
staff_member=self.staff_member1
41+
)
42+
self.assertIsNotNone(reschedule_history.id_request)
43+
44+
45+
class AppointmentRescheduleHistoryValidationTests(BaseTest):
46+
@classmethod
47+
def setUpTestData(cls):
48+
super().setUpTestData()
49+
cls.past_date = timezone.now().date() - timedelta(days=3)
50+
cls.future_date = timezone.now().date() + timedelta(days=3)
51+
52+
def setUp(self):
53+
self.appointment_request = self.create_appt_request_for_sm1()
54+
55+
def test_creation_with_past_date_raises_validation_error(self):
56+
with self.assertRaises(ValidationError):
57+
AppointmentRescheduleHistory.objects.create(
58+
appointment_request=self.appointment_request,
59+
date=self.past_date,
60+
start_time=timezone.now().time(),
61+
end_time=(timezone.now() + timedelta(hours=1)).time(),
62+
staff_member=self.staff_member1
63+
)
64+
65+
def test_invalid_date_format_raises_type_error(self):
66+
with self.assertRaises(TypeError):
67+
AppointmentRescheduleHistory.objects.create(
68+
appointment_request=self.appointment_request,
69+
date="invalid-date",
70+
start_time=timezone.now().time(),
71+
end_time=(timezone.now() + timedelta(hours=1)).time(),
72+
staff_member=self.staff_member1
73+
)
74+
75+
76+
class AppointmentRescheduleHistoryTimingTests(BaseTest):
77+
@classmethod
78+
def setUpTestData(cls):
79+
return super().setUpTestData()
80+
81+
def setUp(self):
82+
self.appointment_request = self.create_appt_request_for_sm1()
83+
self.future_date = timezone.now().date() + timedelta(days=3)
84+
return super().setUp()
85+
86+
def test_still_valid_within_time_frame(self):
87+
reschedule_history = AppointmentRescheduleHistory.objects.create(
88+
appointment_request=self.appointment_request,
89+
date=self.future_date,
90+
start_time=timezone.now().time(),
91+
end_time=(timezone.now() + timedelta(hours=1)).time(),
92+
staff_member=self.staff_member1,
93+
reason_for_rescheduling="Client request",
94+
reschedule_status='pending'
95+
)
96+
self.assertTrue(reschedule_history.still_valid())
97+
98+
def test_still_valid_outside_time_frame(self):
99+
reschedule_history = AppointmentRescheduleHistory.objects.create(
100+
appointment_request=self.appointment_request,
101+
date=self.future_date,
102+
start_time=timezone.now().time(),
103+
end_time=(timezone.now() + timedelta(hours=1)).time(),
104+
staff_member=self.staff_member1,
105+
reason_for_rescheduling="Client request",
106+
reschedule_status='pending'
107+
)
108+
self.assertTrue(reschedule_history.still_valid())
109+
# Simulate passage of time beyond the validity window
110+
reschedule_history.created_at -= timedelta(minutes=6)
111+
reschedule_history.save()
112+
self.assertFalse(reschedule_history.still_valid())

0 commit comments

Comments
 (0)