Files
enterprise-addons/appointment_google_reserve/tests/test_appointment_google_reserve_controller.py
T

1121 lines
50 KiB
Python

import calendar as cal
import json
from datetime import datetime, timedelta
from freezegun import freeze_time
from unittest.mock import patch
from odoo.addons.appointment_google_reserve.tests.common import GoogleReserveCommon
from odoo.tests import common, tagged
@tagged('appointment_google_reserve')
class AppointmentGoogleReserveControllerTest(GoogleReserveCommon, common.HttpCase):
# --------------------------------------
# AVAILABILITIES - RESOURCES
# --------------------------------------
@freeze_time('2022-02-14 07-00-00')
def test_google_reserve_availability_lookup(self):
"""" Test the batch availability endpoint and make sure it returns proper availabilities
based on the appointment type configuration. """
start_slot = datetime(2022, 2, 14, 15, 0, 0)
# Batch (page load)
response = self.opener.get(
f'{self.base_url()}/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/availabilities',
data=json.dumps(
[{
'duration_sec': '3600',
'resource_ids': {
'party_size': party_size,
},
'start_sec': self._to_utc(start_slot + timedelta(hours=i)),
} for i in range(4) for party_size in range(1, 6)]
),
headers={
'Content-Type': 'application/json',
}
)
self.assertEqual(response.status_code, 200)
self.assertTrue(bool(response.json()))
expected_availabilities = [
{'party_size': 1, 'start_time': 15, 'available': True},
{'party_size': 2, 'start_time': 15, 'available': True},
{'party_size': 3, 'start_time': 15, 'available': True},
{'party_size': 4, 'start_time': 15, 'available': True},
{'party_size': 5, 'start_time': 15, 'available': False}, # party size too big
{'party_size': 1, 'start_time': 16, 'available': True},
{'party_size': 2, 'start_time': 16, 'available': True},
{'party_size': 3, 'start_time': 16, 'available': True},
{'party_size': 4, 'start_time': 16, 'available': True},
{'party_size': 5, 'start_time': 16, 'available': False}, # party size too big
{'party_size': 1, 'start_time': 17, 'available': True},
{'party_size': 2, 'start_time': 17, 'available': True},
{'party_size': 3, 'start_time': 17, 'available': True},
{'party_size': 4, 'start_time': 17, 'available': True},
{'party_size': 5, 'start_time': 17, 'available': False}, # party size too big
{'party_size': 1, 'start_time': 18, 'available': False}, # too late
{'party_size': 2, 'start_time': 18, 'available': False}, # too late
{'party_size': 3, 'start_time': 18, 'available': False}, # too late
{'party_size': 4, 'start_time': 18, 'available': False}, # too late
{'party_size': 5, 'start_time': 18, 'available': False}, # too late & party size too big
]
for availability, expected_availability in zip(response.json(), expected_availabilities):
slot_time = availability.get('slot_time')
self.assertEqual(slot_time.get('resource_ids').get('party_size'), expected_availability['party_size'])
expected_start_sec = self._to_utc(start_slot.replace(hour=expected_availability['start_time']))
self.assertEqual(slot_time.get('start_sec'), expected_start_sec)
self.assertEqual(availability.get('available'), expected_availability['available'])
# Single (slot click)
response = self.opener.get(
f'{self.base_url()}/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/availabilities',
data=json.dumps(
[{
'duration_sec': '3600',
'resource_ids': {
'party_size': 4,
},
'start_sec': self._to_utc(start_slot),
}]
),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(bool(response.json()))
availability = response.json()[0]
slot_time = availability.get('slot_time')
self.assertEqual(slot_time.get('resource_ids').get('party_size'), 4)
self.assertEqual(slot_time.get('start_sec'), self._to_utc(start_slot))
self.assertEqual(availability.get('available'), True)
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_availability_lookup_resource_on_leave(self, update_availabilities):
start_slot = datetime(2022, 2, 14, 15, 0, 0)
self.env['resource.calendar.leaves'].create({
'name': 'Table 1 on leave',
'resource_id': self.apt_type_resources_table_1.resource_id.id,
'date_from': datetime(2022, 2, 14, 14, 0, 0),
'date_to': datetime(2022, 2, 14, 17, 0, 0),
})
response = self.opener.get(
f'{self.base_url()}/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/availabilities',
data=json.dumps([{
'duration_sec': '3600',
'resource_ids': {
'party_size': 4,
},
'start_sec': self._to_utc(start_slot),
}]),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(bool(response.json()))
availability = response.json()[0]
slot_time = availability.get('slot_time')
self.assertEqual(slot_time.get('resource_ids').get('party_size'), 4)
self.assertEqual(slot_time.get('start_sec'), self._to_utc(start_slot))
self.assertFalse(availability.get('available'), False)
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_availability_lookup_combinable(self, update_availabilities):
self.apt_type_resources_table_1.write({
'linked_resource_ids': [(4, self.apt_type_resources_table_2.id)],
})
self.apt_type_resources_table_2.write({
'linked_resource_ids': [(4, self.apt_type_resources_table_1.id)],
})
# create some dummy bookings to test more cases
self.env['calendar.event'].create([{
'appointment_type_id': self.apt_type_resource_google.id,
'name': "Booking 1",
'start': datetime(2022, 2, 14, 15, 0, 0),
'stop': datetime(2022, 2, 14, 16, 0, 0),
'booking_line_ids': [(0, 0, {
'appointment_resource_id': self.apt_type_resources_table_1.id,
'capacity_reserved': 3,
'capacity_used': 4
})],
}, {
'appointment_type_id': self.apt_type_resource_google.id,
'name': "Booking 2",
'start': datetime(2022, 2, 14, 16, 0, 0),
'stop': datetime(2022, 2, 14, 17, 0, 0),
'booking_line_ids': [(0, 0, {
'appointment_resource_id': self.apt_type_resources_table_1.id,
'capacity_reserved': 4,
'capacity_used': 4
}), (0, 0, {
'appointment_resource_id': self.apt_type_resources_table_2.id,
'capacity_reserved': 1,
'capacity_used': 2
})],
}])
start_slot = datetime(2022, 2, 14, 15, 0, 0)
# Batch (page load)
response = self.opener.get(
f'{self.base_url()}/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/availabilities',
data=json.dumps(
[{
'duration_sec': '3600',
'resource_ids': {
'party_size': party_size,
},
'start_sec': self._to_utc(start_slot + timedelta(hours=i)),
} for i in range(3) for party_size in range(1, 9)]
),
headers={
'Content-Type': 'application/json',
}
)
self.assertEqual(response.status_code, 200)
self.assertTrue(bool(response.json()))
expected_availabilities = [
{'party_size': 1, 'start_time': 15, 'available': True},
{'party_size': 2, 'start_time': 15, 'available': True},
{'party_size': 3, 'start_time': 15, 'available': False}, # table 1 not available
{'party_size': 4, 'start_time': 15, 'available': False}, # table 1 not available
{'party_size': 5, 'start_time': 15, 'available': False}, # table 1 not available
{'party_size': 6, 'start_time': 15, 'available': False}, # table 1 not available
{'party_size': 7, 'start_time': 15, 'available': False}, # party size too big
{'party_size': 8, 'start_time': 15, 'available': False}, # party size too big
{'party_size': 1, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 2, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 3, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 4, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 5, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 6, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 7, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 8, 'start_time': 16, 'available': False}, # no tables available
{'party_size': 1, 'start_time': 17, 'available': True},
{'party_size': 2, 'start_time': 17, 'available': True},
{'party_size': 3, 'start_time': 17, 'available': True},
{'party_size': 4, 'start_time': 17, 'available': True},
{'party_size': 5, 'start_time': 17, 'available': True},
{'party_size': 6, 'start_time': 17, 'available': True},
{'party_size': 7, 'start_time': 17, 'available': False}, # party size too big
{'party_size': 8, 'start_time': 17, 'available': False}, # party size too big
]
for availability, expected_availability in zip(response.json(), expected_availabilities):
slot_time = availability.get('slot_time')
self.assertEqual(slot_time.get('resource_ids').get('party_size'), expected_availability['party_size'])
expected_start_sec = self._to_utc(start_slot.replace(hour=expected_availability['start_time']))
self.assertEqual(slot_time.get('start_sec'), expected_start_sec)
self.assertEqual(availability.get('available'), expected_availability['available'])
# --------------------------------------
# BOOKINGS - RESOURCES
# --------------------------------------
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_create_cancel_booking(self, update_availabilities):
""" Test a complete create/cancel scenario:
- A first booking is made
- A second booking is made but conflicts with the first one (no more room)
- The first booking is canceled
- The second booking is retried and no-longer conflicts. """
start_slot = datetime(2022, 2, 14, 15, 0, 0)
# STEP 1: create booking
slot_creation_data = {
'confirmation_mode': 'CONFIRMATION_MODE_SYNCHRONOUS',
'duration_sec': '3600',
'merchant_id': str(self.apt_type_resource_google.id),
'resources': {
'party_size': '4',
},
'service_id': str(self.apt_type_resource_google.id),
'start_sec': self._to_utc(start_slot),
}
create_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
'idempotency_token': 'token1',
'slot': slot_creation_data,
'user_information': {
'email': 'john.doe@test.com',
'family_name': 'Doe',
'given_name': 'John',
'telephone': '+32476112233',
'user_id': '1234567890',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(create_data.get('booking_failure')))
self.assertTrue(bool(create_data.get('booking').get('booking_id')))
booking_id = create_data['booking']['booking_id']
calendar_event = self.env['calendar.event'].browse(int(booking_id))
self.assertTrue(bool(calendar_event.exists()))
self.assertEqual(calendar_event.appointment_type_id, self.apt_type_resource_google)
self.assertEqual(calendar_event.total_capacity_reserved, 4)
self.assertEqual(calendar_event.start, start_slot)
self.assertEqual(calendar_event.appointment_booker_id.name, 'John Doe')
self.assertEqual(update_availabilities.call_count, 1)
args, _kwargs = update_availabilities.call_args_list[0]
self.assertEqual(args[1], calendar_event.appointment_type_id)
self.assertEqual(args[2], calendar_event.start)
self.assertEqual(args[3], calendar_event.stop)
# STEP 2: try to create another booking on the same slot, with no more room available
second_booking_data = {
'idempotency_token': 'token2',
'slot': slot_creation_data,
'user_information': {
'email': 'raoulette.poilvache@test.com',
'family_name': 'poilvache',
'given_name': 'raoulette',
'telephone': '+32476112244',
'user_id': '1234567891',
}
}
create_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
**second_booking_data
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.assertEqual(create_data.get('booking_failure').get('cause'), 'SLOT_UNAVAILABLE')
self.assertFalse(bool(create_data.get('booking').get('booking_id')))
# STEP 3: CANCEL THE FIRST BOOKING
cancel_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'status': 'CANCELED',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(cancel_response.status_code, 200)
cancel_data = cancel_response.json()
self.assertFalse(bool(cancel_data.get('booking_failure')))
self.assertEqual(update_availabilities.call_count, 2)
args, _kwargs = update_availabilities.call_args_list[0]
self.assertEqual(args[1], calendar_event.appointment_type_id)
self.assertEqual(args[2], calendar_event.start)
self.assertEqual(args[3], calendar_event.stop)
self.env['calendar.event'].flush_model()
self.assertFalse(calendar_event.active)
# step 3b: cancel the first booking again (custom booking_failure)
cancel_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'status': 'CANCELED',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(cancel_response.status_code, 200)
cancel_data = cancel_response.json()
self.assertEqual(cancel_data.get('booking_failure').get('cause'), 'BOOKING_ALREADY_CANCELLED')
# did not call update availabilities as no update
self.assertEqual(update_availabilities.call_count, 2)
# step 4: retry second booking, should work now
create_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
'idempotency_token': 'token3',
**second_booking_data
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(create_data.get('booking_failure')))
self.assertTrue(bool(create_data.get('booking').get('booking_id')))
booking_id = create_data['booking']['booking_id']
calendar_event_2 = self.env['calendar.event'].browse(int(booking_id))
self.assertNotEqual(calendar_event, calendar_event_2)
self.assertTrue(bool(calendar_event_2.exists()))
self.assertEqual(calendar_event_2.appointment_type_id, self.apt_type_resource_google)
self.assertEqual(update_availabilities.call_count, 3)
args, _kwargs = update_availabilities.call_args_list[1]
self.assertEqual(args[1], calendar_event_2.appointment_type_id)
self.assertEqual(args[2], calendar_event_2.start)
self.assertEqual(args[3], calendar_event_2.stop)
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_create_update_booking(self, update_availabilities):
""" Test a complete create/update scenario:
- A first booking is made (2 people, 3PM)
- It is then updated (4 people, 5PM)
- It is updated again for 5 people but there is not enough room (USER_OVER_BOOKING_LIMIT)
- It is updated again for another time outside appointment slots (SLOT_UNAVAILABLE)"""
start_slot = datetime(2022, 2, 14, 15, 0, 0)
# STEP 1: create booking (2 people, 3PM)
create_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
'idempotency_token': 'token1',
'slot': {
'confirmation_mode': 'CONFIRMATION_MODE_SYNCHRONOUS',
'duration_sec': '3600',
'merchant_id': str(self.apt_type_resource_google.id),
'resources': {
'party_size': '2',
},
'service_id': str(self.apt_type_resource_google.id),
'start_sec': self._to_utc(start_slot)
},
'user_information': {
'email': 'john.doe@test.com',
'family_name': 'Doe',
'given_name': 'John',
'telephone': '+32476112233',
'user_id': '1234567890',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(create_data.get('booking_failure')))
self.assertTrue(bool(create_data.get('booking').get('booking_id')))
booking_id = create_data['booking']['booking_id']
calendar_event = self.env['calendar.event'].browse(int(booking_id))
self.assertTrue(bool(calendar_event.exists()))
self.assertEqual(calendar_event.appointment_type_id, self.apt_type_resource_google)
self.assertEqual(calendar_event.total_capacity_reserved, 2)
self.assertEqual(calendar_event.start, start_slot)
self.assertEqual(update_availabilities.call_count, 1)
args, _kwargs = update_availabilities.call_args_list[0]
self.assertEqual(args[1], calendar_event.appointment_type_id)
self.assertEqual(args[2], calendar_event.start)
self.assertEqual(args[3], calendar_event.stop)
# STEP 2: update booking -> 4 people, 5PM
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'duration_sec': '3600',
'resources': {
'party_size': 4,
},
'start_sec': self._to_utc(start_slot + timedelta(hours=2)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertFalse(bool(update_data.get('booking_failure')))
calendar_event.flush_recordset()
self.assertEqual(calendar_event.total_capacity_reserved, 4)
self.assertEqual(calendar_event.start, start_slot + timedelta(hours=2))
self.assertEqual(update_availabilities.call_count, 2)
args, _kwargs = update_availabilities.call_args_list[1]
self.assertEqual(args[1], calendar_event.appointment_type_id)
self.assertEqual(
args[2],
start_slot,
"Should update availabilities on Google side based from old event start and not new one"
)
self.assertEqual(args[3], calendar_event.stop)
# STEP 3: update booking -> 5 people -> not enough room
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'resources': {
'party_size': 5,
},
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertEqual(update_data.get('booking_failure').get('cause'), 'USER_OVER_BOOKING_LIMIT')
calendar_event.flush_recordset()
self.assertEqual(calendar_event.total_capacity_reserved, 4)
self.assertEqual(calendar_event.start, start_slot + timedelta(hours=2))
# did not call update availabilities as no update
self.assertEqual(update_availabilities.call_count, 2)
# STEP 4: update booking -> 6PM -> outside appointment type time slots
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'duration_sec': '3600',
'start_sec': self._to_utc(start_slot + timedelta(hours=3)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertEqual(update_data.get('booking_failure').get('cause'), 'SLOT_UNAVAILABLE')
calendar_event.flush_recordset()
self.assertEqual(calendar_event.total_capacity_reserved, 4)
self.assertEqual(calendar_event.start, start_slot + timedelta(hours=2))
# did not call update availabilities as no update
self.assertEqual(update_availabilities.call_count, 2)
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_create_update_booking_combinable(self, update_availabilities):
""" Test a complete create/update scenario for combinable resources (more complex backend logic):
- A first booking is made (6 people)
- It is then updated (11 people)
- It is updated again for 15 people but there is not enough room (USER_OVER_BOOKING_LIMIT)
- And finally updated again to 4 people to check that we also handle removing booking lines. """
self.apt_type_resources_table_3 = self.env['appointment.resource'].create({
'appointment_type_ids': self.apt_type_resource_google.ids,
'capacity': 6,
'name': 'Table 3',
'sequence': 3,
'linked_resource_ids': [
(4, self.apt_type_resources_table_1.id),
(4, self.apt_type_resources_table_2.id),
],
})
self.apt_resources_google += self.apt_type_resources_table_3
self.apt_type_resources_table_1.write({
'linked_resource_ids': [
(4, self.apt_type_resources_table_2.id),
(4, self.apt_type_resources_table_3.id),
],
})
self.apt_type_resources_table_2.write({
'linked_resource_ids': [
(4, self.apt_type_resources_table_1.id),
(4, self.apt_type_resources_table_3.id),
],
})
# STEP 1: create booking (6 people)
create_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
'idempotency_token': 'token1',
'slot': {
'confirmation_mode': 'CONFIRMATION_MODE_SYNCHRONOUS',
'duration_sec': '3600',
'merchant_id': str(self.apt_type_resource_google.id),
'resources': {
'party_size': '6',
},
'service_id': str(self.apt_type_resource_google.id),
'start_sec': self._to_utc(datetime(2022, 2, 14, 15, 0, 0))
},
'user_information': {
'email': 'john.doe@test.com',
'family_name': 'Doe',
'given_name': 'John',
'telephone': '+32476112233',
'user_id': '1234567890',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(create_data.get('booking_failure')))
self.assertTrue(bool(create_data.get('booking').get('booking_id')))
booking_id = create_data['booking']['booking_id']
calendar_event = self.env['calendar.event'].browse(int(booking_id))
self.assertTrue(bool(calendar_event.exists()))
self.assertEqual(calendar_event.appointment_type_id, self.apt_type_resource_google)
self.assertEqual(calendar_event.total_capacity_reserved, 6)
self.assertEqual(
len(calendar_event.booking_line_ids), 1,
"Table 3 can accommodate 6 people -> should only use one booking"
)
self.assertEqual(
calendar_event.booking_line_ids[0].appointment_resource_id,
self.apt_type_resources_table_3,
)
# STEP 2: update booking -> 11 people
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'resources': {
'party_size': 11,
},
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertFalse(bool(update_data.get('booking_failure')))
calendar_event.flush_recordset()
self.assertEqual(calendar_event.total_capacity_reserved, 11)
self.assertEqual(
len(calendar_event.booking_line_ids), 3,
"By combining all 3 tables we can accommodate 11 people"
)
for resource in self.apt_resources_google:
self.assertEqual(
len(calendar_event.booking_line_ids.filtered(
lambda booking: booking.appointment_resource_id == resource
)),
1,
)
# STEP 3: update booking -> 15 people -> not enough room
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'resources': {
'party_size': 15,
},
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertEqual(update_data.get('booking_failure').get('cause'), 'USER_OVER_BOOKING_LIMIT')
calendar_event.flush_recordset()
self.assertEqual(calendar_event.total_capacity_reserved, 11)
# STEP 4: update booking -> 4 people
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'resources': {
'party_size': 4,
},
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = update_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(update_data.get('booking_failure')))
calendar_event.flush_recordset()
self.assertEqual(calendar_event.total_capacity_reserved, 4)
self.assertEqual(
len(calendar_event.booking_line_ids), 1,
"Table 1 can accommodate 4 people -> should only use one booking"
)
self.assertEqual(
calendar_event.booking_line_ids[0].appointment_resource_id,
self.apt_type_resources_table_1,
)
# STEP 5: test creation process that combines tables
create_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
'idempotency_token': 'token2',
'slot': {
'confirmation_mode': 'CONFIRMATION_MODE_SYNCHRONOUS',
'duration_sec': '3600',
'merchant_id': str(self.apt_type_resource_google.id),
'resources': {
'party_size': '11',
},
'service_id': str(self.apt_type_resource_google.id),
'start_sec': self._to_utc(datetime(2022, 2, 14, 16, 0, 0))
},
'user_information': {
'email': 'john.doe@test.com',
'family_name': 'Doe',
'given_name': 'John',
'telephone': '+32476112233',
'user_id': '1234567890',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(create_data.get('booking_failure')))
self.assertTrue(bool(create_data.get('booking').get('booking_id')))
booking_id = create_data['booking']['booking_id']
calendar_event = self.env['calendar.event'].browse(int(booking_id))
self.assertTrue(bool(calendar_event.exists()))
self.assertEqual(calendar_event.appointment_type_id, self.apt_type_resource_google)
self.assertEqual(calendar_event.total_capacity_reserved, 11)
self.assertEqual(
len(calendar_event.booking_line_ids), 3,
"By combining all 3 tables we can accommodate 11 people"
)
self.assertEqual(
calendar_event.booking_line_ids.mapped('capacity_reserved'),
[5, 4, 2]
)
self.assertEqual(
calendar_event.booking_line_ids.mapped('capacity_used'),
[6, 4, 2]
)
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_move_booking(self, update_availabilities):
""" When moving a booking to a new date, if the currently used resources are not available
at the new time, make sure we check if other resources are available. """
bookings = self.env['calendar.event'].create([{
'appointment_type_id': self.apt_type_resource_google.id,
'name': "Booking 1",
'start': datetime(2022, 2, 14, 15, 0, 0),
'stop': datetime(2022, 2, 14, 16, 0, 0),
'total_capacity_reserved': 2,
'booking_line_ids': [(0, 0, {
'appointment_resource_id': self.apt_type_resources_table_2.id,
'capacity_reserved': 2,
'capacity_used': 2,
})],
}, {
'appointment_type_id': self.apt_type_resource_google.id,
'name': "Booking 2",
'start': datetime(2022, 2, 14, 16, 0, 0),
'stop': datetime(2022, 2, 14, 17, 0, 0),
'total_capacity_reserved': 2,
'booking_line_ids': [(0, 0, {
'appointment_resource_id': self.apt_type_resources_table_2.id,
'capacity_reserved': 2,
'capacity_used': 2,
})],
}, {
'appointment_type_id': self.apt_type_resource_google.id,
'name': "Booking 3",
'start': datetime(2022, 2, 14, 17, 0, 0),
'stop': datetime(2022, 2, 14, 18, 0, 0),
'total_capacity_reserved': 6,
'booking_line_ids': [(0, 0, {
'appointment_resource_id': self.apt_type_resources_table_1.id,
'capacity_reserved': 4,
'capacity_used': 4
}), (0, 0, {
'appointment_resource_id': self.apt_type_resources_table_2.id,
'capacity_reserved': 1,
'capacity_used': 2
})],
}])
edited_booking = bookings[0]
# STEP 1: update to take a different time where used resource is not available
# -> but another table is available
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{edited_booking.id}/update',
data=json.dumps({
'booking': {
'booking_id': str(edited_booking.id),
'slot': {
'duration_sec': '3600',
'start_sec': self._to_utc(datetime(2022, 2, 14, 16, 0, 0)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = update_response.json()
edited_booking.flush_recordset()
self.assertFalse(bool(update_data.get('booking_failure')))
self.assertEqual(edited_booking.total_capacity_reserved, 2)
self.assertEqual(
len(edited_booking.booking_line_ids), 1,
"Table 1 is available can accommodate 4 people -> should only use one booking"
)
self.assertEqual(
edited_booking.booking_line_ids[0].appointment_resource_id,
self.apt_type_resources_table_1,
)
# STEP 2: update to take a different time where no resources are available
# -> should fail and not update the existing calendar.event
update_response = self.url_open(
f'/appointment/{self.apt_type_resource_google.id}/{self.apt_type_resource_google.google_reserve_access_token}/google_reserve/booking/{edited_booking.id}/update',
data=json.dumps({
'booking': {
'booking_id': str(edited_booking.id),
'slot': {
'duration_sec': '3600',
'start_sec': self._to_utc(datetime(2022, 2, 14, 17, 0, 0)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = update_response.json()
self.assertEqual(update_data.get('booking_failure').get('cause'), 'SLOT_UNAVAILABLE')
edited_booking.flush_recordset()
self.assertEqual(edited_booking.total_capacity_reserved, 2)
self.assertEqual(
len(edited_booking.booking_line_ids), 1,
)
self.assertEqual(
edited_booking.booking_line_ids[0].appointment_resource_id,
self.apt_type_resources_table_1,
)
# --------------------------------------
# AVAILABILITIES - STAFF USERS
# --------------------------------------
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_availability_lookup_staff(self, update_availabilities):
"""" Same logic but for staff users and not resources.
Main difference is that we don't have a party size. """
start_slot = datetime(2022, 2, 14, 15, 0, 0)
response = self.opener.get(
f'{self.base_url()}/appointment/{self.apt_type_staff_google.id}/{self.apt_type_staff_google.google_reserve_access_token}/google_reserve/availabilities',
data=json.dumps(
[{
'duration_sec': '3600',
'start_sec': self._to_utc(start_slot + timedelta(hours=i)),
} for i in range(4)]
),
headers={
'Content-Type': 'application/json',
}
)
self.assertEqual(response.status_code, 200)
self.assertTrue(bool(response.json()))
expected_availabilities = [
{'start_time': 15, 'available': True},
{'start_time': 16, 'available': True},
{'start_time': 17, 'available': True},
{'start_time': 18, 'available': False}, # False
]
for availability, expected_availability in zip(response.json(), expected_availabilities):
slot_time = availability.get('slot_time')
expected_start_sec = self._to_utc(start_slot.replace(hour=expected_availability['start_time']))
self.assertEqual(slot_time.get('start_sec'), expected_start_sec)
self.assertEqual(availability.get('available'), expected_availability['available'])
# add some meetings to our staff
self.env['calendar.event'].create([{
'name': "Manager Meeting",
'start': datetime(2022, 2, 14, 15, 0, 0),
'stop': datetime(2022, 2, 14, 16, 0, 0),
'partner_ids': [(4, self.apt_manager.partner_id.id)],
}, {
'name': "Whole Company Meeting",
'start': datetime(2022, 2, 14, 16, 0, 0),
'stop': datetime(2022, 2, 14, 17, 0, 0),
'partner_ids': [
(4, self.apt_manager.partner_id.id),
(4, self.staff_user_bxls.partner_id.id),
],
}])
response = self.opener.get(
f'{self.base_url()}/appointment/{self.apt_type_staff_google.id}/{self.apt_type_staff_google.google_reserve_access_token}/google_reserve/availabilities',
data=json.dumps(
[{
'duration_sec': '3600',
'start_sec': self._to_utc(start_slot + timedelta(hours=i)),
} for i in range(3)]
),
headers={
'Content-Type': 'application/json',
}
)
self.assertEqual(response.status_code, 200)
self.assertTrue(bool(response.json()))
expected_availabilities = [
{'start_time': 15, 'available': True}, # staff_user_bxls is available
{'start_time': 16, 'available': False}, # no staff users available
{'start_time': 17, 'available': True}, # both staff users are available
]
for availability, expected_availability in zip(response.json(), expected_availabilities):
with self.subTest(
start_time=expected_availability['start_time'],
available=expected_availability['available'],
):
slot_time = availability.get('slot_time')
expected_start_sec = self._to_utc(start_slot.replace(hour=expected_availability['start_time']))
self.assertEqual(slot_time.get('start_sec'), expected_start_sec)
self.assertEqual(availability.get('available'), expected_availability['available'])
# --------------------------------------
# AVAILABILITIES - STAFF USERS
# --------------------------------------
@freeze_time('2022-02-14 07-00-00')
@patch('odoo.addons.appointment_google_reserve.tools.google_reserve_iap.GoogleReserveIAP.update_availabilities', autospec=True)
def test_google_reserve_create_update_booking_staff(self, update_availabilities):
""" Test a complete create/update scenario:
- A first booking is made (3PM)
- It is then updated (4PM)
- It is updated again (5PM) but the staff user is no longer available (SLOT_UNAVAILABLE)
- And finally updated again for another time outside appointment slots (SLOT_UNAVAILABLE) """
start_slot = datetime(2022, 2, 14, 15, 0, 0)
# only keep one staff user to make checks more simple
self.apt_type_staff_google.staff_user_ids = [(6, 0, [self.staff_user_bxls.id])]
self.env['calendar.event'].create({
'name': "Whole Company Meeting",
'start': datetime(2022, 2, 14, 17, 0, 0),
'stop': datetime(2022, 2, 14, 18, 0, 0),
'partner_ids': [(4, self.staff_user_bxls.partner_id.id)],
})
# STEP 1: create booking (3PM)
create_response = self.url_open(
f'/appointment/{self.apt_type_staff_google.id}/{self.apt_type_staff_google.google_reserve_access_token}/google_reserve/booking/create',
data=json.dumps({
'idempotency_token': 'token1',
'slot': {
'confirmation_mode': 'CONFIRMATION_MODE_SYNCHRONOUS',
'duration_sec': '3600',
'merchant_id': str(self.apt_type_staff_google.id),
'service_id': str(self.apt_type_staff_google.id),
'start_sec': self._to_utc(start_slot)
},
'user_information': {
'email': 'john.doe@test.com',
'family_name': 'Doe',
'given_name': 'John',
'telephone': '+32476112233',
'user_id': '1234567890',
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(create_response.status_code, 200)
create_data = create_response.json()
self.env['calendar.event'].flush_model()
self.assertFalse(bool(create_data.get('booking_failure')))
self.assertTrue(bool(create_data.get('booking').get('booking_id')))
booking_id = create_data['booking']['booking_id']
calendar_event = self.env['calendar.event'].browse(int(booking_id))
self.assertTrue(bool(calendar_event.exists()))
self.assertEqual(calendar_event.appointment_type_id, self.apt_type_staff_google)
self.assertEqual(calendar_event.start, start_slot)
self.assertIn(self.staff_user_bxls.partner_id, calendar_event.partner_ids)
self.assertEqual(update_availabilities.call_count, 1)
args, _kwargs = update_availabilities.call_args_list[0]
self.assertEqual(args[1], calendar_event.appointment_type_id)
self.assertEqual(args[2], calendar_event.start)
self.assertEqual(args[3], calendar_event.stop)
# STEP 2: update booking -> 4PM
update_response = self.url_open(
f'/appointment/{self.apt_type_staff_google.id}/{self.apt_type_staff_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'duration_sec': '3600',
'start_sec': self._to_utc(start_slot + timedelta(hours=1)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertFalse(bool(update_data.get('booking_failure')))
calendar_event.flush_recordset()
self.assertEqual(calendar_event.start, start_slot + timedelta(hours=1))
self.assertEqual(update_availabilities.call_count, 2)
# STEP 3: update booking -> 5PM -> staff user is busy
update_response = self.url_open(
f'/appointment/{self.apt_type_staff_google.id}/{self.apt_type_staff_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'duration_sec': '3600',
'start_sec': self._to_utc(start_slot + timedelta(hours=2)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertEqual(update_data.get('booking_failure').get('cause'), 'SLOT_UNAVAILABLE')
calendar_event.flush_recordset()
self.assertEqual(calendar_event.start, start_slot + timedelta(hours=1))
# did not call update availabilities as no update
self.assertEqual(update_availabilities.call_count, 2)
# STEP 4: update booking -> 6PM -> outside appointment type time slots
update_response = self.url_open(
f'/appointment/{self.apt_type_staff_google.id}/{self.apt_type_staff_google.google_reserve_access_token}/google_reserve/booking/{booking_id}/update',
data=json.dumps({
'booking': {
'booking_id': str(booking_id),
'slot': {
'duration_sec': '3600',
'start_sec': self._to_utc(start_slot + timedelta(hours=3)),
},
}
}),
headers={
'Content-Type': 'application/json',
},
)
self.assertEqual(update_response.status_code, 200)
update_data = json.loads(update_response.content)
self.assertEqual(update_data.get('booking_failure').get('cause'), 'SLOT_UNAVAILABLE')
calendar_event.flush_recordset()
self.assertEqual(calendar_event.start, start_slot + timedelta(hours=1))
# did not call update availabilities as no update
self.assertEqual(update_availabilities.call_count, 2)
# --------------------------------------
# UTILS
# --------------------------------------
def _to_utc(self, slot_datetime):
return str(cal.timegm((slot_datetime).timetuple()))