File: //home/arjun/projects/buyercall/buyercall/blueprints/api2/doc/endpoints/appointments.py
import logging
import time
import uuid
from flask import (
Blueprint,
make_response,
request,
jsonify)
from datetime import datetime
from flask import Blueprint, jsonify, make_response
from buyercall.lib.util_rest import rest_partnership_account, rest_partnership, requires_auth, rest_is_partnership_account, rest_is_partnership
from flask_restx import Resource, reqparse
from buyercall.blueprints.api2.doc import serializers
from buyercall.blueprints.api2.restx import api
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
from buyercall.blueprints.appointments.models import Appointment, AppointmentSlot
from buyercall.lib.util_datetime import week_day
from buyercall.blueprints.user.models import User
from sqlalchemy import func, or_, and_, extract
log = logging.getLogger(__name__)
ns = api.namespace('Appointments', description='Operations related to appointments.', path='/accounts')
get_existing_appointment_parser = reqparse.RequestParser()
get_existing_appointment_parser.add_argument('user_id', type=int, location='args', help='The appointment user id')
get_existing_appointment_parser.add_argument('contact_id', type=int, location='args', help='The appointment contact id')
get_existing_appointment_parser.add_argument('appointment_type', type=str, location='args', help='The appointment type')
get_existing_appointment_parser.add_argument('appointment_date', type=str, location='args', help='The appointment date')
get_existing_appointment_parser.add_argument('appointment_start_time', type=str, location='args',
help='The appointment start time')
get_available_appointment_parser = reqparse.RequestParser()
get_available_appointment_parser.add_argument('user_id', type=int, location='args', help='The appointment user id')
get_available_appointment_parser.add_argument('appointment_type', type=str, location='args', help='The appointment type',
default='general')
get_available_appointment_parser.add_argument('date', type=str, location='args', help='The preferred appointment date')
@ns.route('/<int:paid>/appointments')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'paid': 'The partnership account Id'})
class ApiAppointmentsCollection(Resource):
@api.expect(serializers.appointment_add, validate=True)
@api.response(200, 'Appointment successfully created.')
@requires_auth
def post(self, paid):
"""
Creates an appointment for a partnership account or partnership account user.
<p>
The Appointment API POST endpoint should be used to create an appointment for either a partnership account
using the partnership account's operating hours or for a partnership account user using the user's schedule.
</p>
<br />
<p>
You will require a partner authentication token and a partner account to make a successful request.
Providing a user id is not mandatory and an appointment can be created without a user id. If you would like
create an appointment for a specific user please make sure to provide a user id in the request body.
A response will be returned, similar to the example below, based on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"appointment_date": "2021-05-31",
"appointment_end_time": "11:30",
"appointment_sid": "8c807b85-4340-47f0-b686-3c693b0b8e99",
"appointment_start_time": "11:00",
"appointment_timezone": "US/Eastern",
"partnership_account_id": 1
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
# Find and get the partnership account
if rest_partnership.partner_type == 'system':
partnership_account = PartnershipAccount \
.query.filter(PartnershipAccount.id == paid).first()
else:
partnership_account = PartnershipAccount.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)).first()
# Set the parameters sent in he request
received = request.json
# Check for partnership account and that request body data was provided
if received is not None and partnership_account is not None:
contact_id = received['contact_id']
date = received['appointment_date']
start_time = received['appointment_start_time']
if 'user_id' in received:
user = received['user_id']
else:
user = None
if 'appointment_type' in received:
appointment_type = received['appointment_type']
else:
appointment_type = 'general'
result = Appointment.create(partnership_account.id,
contact_id, date, start_time,
appointment_type, user)
if type(result) == uuid.UUID:
new_appointment = Appointment.query.filter(Appointment.sid == result).first()
appointment_slot = AppointmentSlot.query\
.filter(AppointmentSlot.appointment_id == new_appointment.id).first()
date_formatted = datetime.strftime(new_appointment.appointment_date, '%Y-%m-%d')
response = jsonify(
appointment_sid=result,
partnership_account_id=partnership_account.id,
appointment_date=date_formatted,
appointment_start_time=appointment_slot.start_time,
appointment_end_time=appointment_slot.end_time,
appointment_timezone=new_appointment.appointment_timezone
)
response.status_code = 200
return response
elif result == -2:
return api.abort(code=400, message="Error creating appointment. The user does not have availability"
" at the requested date and time.")
elif result == -3:
return api.abort(code=404, message="Error creating appointment. The user does not seem to exist.")
elif result == -4:
return api.abort(code=400, message="Error creating appointment. The partnership account does not "
"have availability at the requested date and time.")
else:
return api.abort(code=404, message="Error creating appointment.")
else:
api.abort(code=404, message="Error creating appointment. Partnership account not found.")
else:
api.abort(code=401)
@api.expect(get_existing_appointment_parser)
@requires_auth
def get(self, paid):
"""
Retrieve all appointments set for a partnership account or partnership account user.
<p>
The Appointments API GET endpoint should be used to retrieve all the appointments for a specific partnership
account or partnership account user. Add the user id as parameter to return user specific appointments otherwise
all appointments will be returned for the specific partnership account. You can also choose to return all
appointments for a specific consumer by providing an optional contact id parameter, appointment date and
appointment start time.
</p>
<br />
<p>
You require a partner authentication token and a partner account id to make a successful request.
Optionally you can also add a user id, appointment type, appointment date, appointment time and/or contact id
as parameters to refine the results.
A response will be returned, similar to the example below, based on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"appointments": [
{
"appointment_date": "2021-05-31",
"appointment_end_time": "10:30",
"appointment_sid": "fc76039c-e86d-24ca-db11-131ba35898a4",
"appointment_start_time": "10:00",
"appointment_timezone": "US/Eastern",
"appointment_type": "general",
"contact_id": 26,
"created_on": "2021-04-07 21:51:02",
"deactivated_on": null,
"is_deactivated": false,
"updated_on": "2021-04-07 21:51:02",
"user_id": null
},
{
"appointment_date": "2021-05-31",
"appointment_end_time": "10:30",
"appointment_sid": "6fe5051b-f1cd-987a-2243-b2d89d897843",
"appointment_start_time": "10:00",
"appointment_timezone": "US/Eastern",
"appointment_type": "general",
"contact_id": 40,
"created_on": "2021-04-07 21:51:02",
"deactivated_on": null,
"is_deactivated": false,
"updated_on": "2021-04-07 21:51:02",
"user_id": null
}
]
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
# Find and get the partnership account
if rest_partnership.partner_type == 'system':
partnership_account = PartnershipAccount \
.query.filter(PartnershipAccount.id == paid).first()
else:
partnership_account = PartnershipAccount.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)).first()
if partnership_account is not None:
# Get any parameters sent with the GET request
args = get_existing_appointment_parser.parse_args()
# Set the filtering structure to be used to get all appointments
queries = [Appointment.partnership_account_id == partnership_account.id]
if 'user_id' in args and args['user_id'] is not None:
user_id = args['user_id']
queries.append(Appointment.user_id == user_id)
if 'contact_id' in args and args['contact_id'] is not None:
contact_id = args['contact_id']
queries.append(Appointment.contact_id == contact_id)
if 'appointment_type' in args and args['appointment_type'] is not None:
appointment_type = args['appointment_type']
queries.append(Appointment.appointment_type == appointment_type)
if 'appointment_date' in args and args['appointment_date'] is not None:
try:
datetime.strptime(args['appointment_date'], '%Y-%m-%d')
appointment_date = args['appointment_date']
queries.append(Appointment.appointment_date == appointment_date)
except ValueError:
api.abort(code=400, message="An incorrect appointment date format was provided. "
"It should be YYYY-MM-DD")
# Set start time variable as empty to avoid errors when not provided
appointment_start_time = ''
if 'appointment_start_time' in args and args['appointment_start_time'] is not None:
if len(args['appointment_start_time']) == 5:
appointment_start_time = args['appointment_start_time']
else:
api.abort(code=400, message="An incorrect appointment start time format was provided. "
"It should be HH:MM")
if appointment_start_time:
appointments = Appointment.query.filter(*queries).join(AppointmentSlot)\
.filter(AppointmentSlot.start_time == appointment_start_time).all()
else:
# Get all the appointments based on the filtering established above
appointments = Appointment.query.filter(*queries).all()
appointment_list = []
# run through the appointments returned and add them to a appointment list above
if appointments is not None:
for appointment in appointments:
for appointment_slot in appointment.appointment_slot:
format_date = datetime.strftime(appointment.appointment_date, '%Y-%m-%d')
add_appointment = {
'appointment_sid': appointment.sid,
'created_on': appointment.created_datetime,
'updated_on': appointment.updated_datetime,
'appointment_date': format_date,
'appointment_start_time': appointment_slot.start_time,
'appointment_end_time': appointment_slot.end_time,
'appointment_timezone': appointment.appointment_timezone,
'appointment_type': appointment.appointment_type,
'contact_id': appointment.contact_id,
'user_id': appointment.user_id,
'is_deactivated': appointment.is_deactivated,
'deactivated_on': appointment.deactivated_on
}
appointment_list.append(add_appointment)
return jsonify(
appointments=appointment_list
)
else:
api.abort(code=404, message="Error retrieving appointment. Partnership account not found.")
else:
api.abort(code=401)
@ns.route('/<int:paid>/appointments/availability')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'paid': 'The partnership account Id'})
class ApiAppointmentsAvailableCollection(Resource):
@api.response(200, 'Appointment successfully created.')
@api.expect(get_available_appointment_parser)
@requires_auth
def get(self, paid):
"""
Retrieve all available appointments set for a partnership account or partnership account user.
<p>
The Appointments Availability API GET endpoint should be used to retrieve all available appointment slots for
a specific partnership account or partnership account user. Available slots up until 2 months in the future
will be returned. You will be able to filter the request based on the user or appointment type by providing
a user id and/or appointment type parameters. You are also able to specify a date in the future to find
availability based on a future date instead of using today's date to availability. When specifying a date
please use the format yyyy-mm-dd.
</p>
<br />
<p>
You require a partner authentication token and a partner account id to make a successful request.
Optionally you can also add a user id, appointment type or date. A response will be returned,
similar to the example below, based on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"available_appointment_slots": [
{
"date": "2021-04-21",
"day_of_week": "wednesday",
"end_time": "9:00",
"start_time": "8:30"
},
{
"date": "2021-04-21",
"day_of_week": "wednesday",
"end_time": "9:30",
"start_time": "9:00"
}
]
}
</pre>
"""
if rest_is_partnership and rest_partnership is not None:
# Find and get the partnership account
if rest_partnership.partner_type == 'system':
partnership_account = PartnershipAccount \
.query.filter(PartnershipAccount.id == paid).first()
else:
partnership_account = PartnershipAccount.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)).first()
if partnership_account is not None:
# Get any parameters sent with the GET request
args = get_available_appointment_parser.parse_args()
appointment_type = None
user_id = None
appointment_date = None
if 'user_id' in args:
user_id = args['user_id']
if 'appointment_type' in args:
appointment_type = args['appointment_type']
if 'date' in args and args['date'] is not None:
try:
datetime.strptime(args['date'], '%Y-%m-%d')
appointment_date = args['date']
except ValueError:
api.abort(code=400, message="An incorrect appointment date format was provided. "
"It should be YYYY-MM-DD")
else:
appointment_date = None
if user_id:
available_appointments = User.user_available_appointment_slots(user_id, appointment_date)
else:
available_appointments = PartnershipAccount.partnership_account_available_appointment_slots(
partnership_account.id, appointment_type, appointment_date)
# run through the appointments returned and add them to a appointment list above
available_appointment_slots = []
for slot in available_appointments:
format_date = slot['date'][:-9]
format_weekday = week_day(slot['weekday'])
add_appointment = {
'date': format_date,
'day_of_week': format_weekday,
'start_time': slot['start_time'],
'end_time': slot['end_time']
}
available_appointment_slots.append(add_appointment)
return jsonify(
available_appointment_slots=available_appointment_slots
)
else:
api.abort(code=404, message="Error retrieving appointment. Partnership account not found.")
else:
api.abort(code=401)
@ns.route('/<int:paid>/appointment/<uuid:appsid>')
@api.doc(responses={200: 'OK',
400: 'Error performing operation.',
401: 'Unauthorized request.'},
params={'appsid': 'The appointment SID',
'paid': 'The partnership account Id'})
class ApiAppointmentsSpecific(Resource):
@api.expect(serializers.appointment_edit, validate=True)
@api.response(200, 'Appointment successfully updated.')
@requires_auth
def put(self, paid, appsid):
"""
Updates an appointment for a partnership account or partnership account user.
<p>The Appointments API PUT endpoint can be used to update an appointment for a specific partnership account or
partnership account user.</p>
<br />
<p>
You require a valid partnership account id and appointment sid to
make a successful request. A response code will be returned
on a successful request.
</p>
"""
if rest_is_partnership and rest_partnership is not None:
# Find and get the partnership account
if rest_partnership.partner_type == 'system':
partnership_account = PartnershipAccount \
.query.filter(PartnershipAccount.id == paid).first()
else:
partnership_account = PartnershipAccount.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)).first()
if partnership_account is not None:
appointment = Appointment.query\
.filter(and_(Appointment.partnership_account_id == partnership_account.id,
Appointment.sid == appsid)).first()
appointment_slot = AppointmentSlot.query\
.filter(AppointmentSlot.appointment_id == appointment.id).first()
if appointment is not None:
received = request.json
if 'appointment_date' in received:
date = received['appointment_date']
else:
date = appointment.appointment_date
if 'appointment_start_time' in received:
start_time = received['appointment_start_time']
else:
start_time = appointment_slot.start_time
if 'user_id' in received:
user_id = received['user_id']
else:
user_id = appointment.user_id
if 'appointment_type' in received:
appointment_type = received['appointment_type']
else:
appointment_type = appointment.appointment_type
result = Appointment.update(appointment.id, appointment_slot.id, partnership_account.id,
date, start_time, appointment_type, user_id)
if result is True:
return True, 204
elif result == -1:
api.abort(code=400, message="Error updating appointment. The provided date and time is"
" not available.")
else:
return api.abort(code=404, message="Error updating appointment.")
else:
api.abort(code=404, message="Error updating appointment. Appointment not found.")
else:
api.abort(code=404, message="Error retrieving appointment. Partnership account not found.")
else:
api.abort(code=401)
@api.response(200, 'Appointment successfully deleted.')
@requires_auth
def delete(self, paid, appsid):
"""
Removes an appointment for a partnership account or partnership account user.
<p>The Appointment API DELETE endpoint can be used to deactive an appointment for a specific partnership
account or partnership account user.</p>
<br />
<p>
You require a valid partnership account id and a appointment sid to
make a successful request. A response code will be returned
on a successful request.
</p>
"""
if rest_is_partnership and rest_partnership is not None:
# Find and get the partnership account
if rest_partnership.partner_type == 'system':
partnership_account = PartnershipAccount \
.query.filter(PartnershipAccount.id == paid).first()
else:
partnership_account = PartnershipAccount.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)).first()
if partnership_account is not None:
appointment = Appointment.query.filter(Appointment.sid == appsid).first()
if appointment is not None:
result = Appointment.deactivate(appointment.id, partnership_account.id)
if result:
return True, 204
else:
return api.abort(code=400, message="Error deactivating appointment.")
else:
api.abort(code=404, message="Error deactivating appointment. Appointment not found.")
else:
api.abort(code=404, message="Error retrieving appointment. Partnership account not found.")
else:
api.abort(code=401)
@api.response(200, 'Appointment successfully deleted.')
@requires_auth
def get(self, paid, appsid):
"""
Retrieve the details for a specific appointment for a partnership account or partnership account user.
<p>The Appointment API GET endpoint can be used to find and return the details for an appointment
for a specific partnership account or partnership account user.
account or partnership account user.</p>
<br />
<p>
You require a valid partnership account id and a appointment sid to
make a successful request. A response will be returned,
similar to the example below, based on a successful request:
<br />
<br />
</p>
<pre class="code-background" style="color: white">
{
"appointment_date": "2021-04-25",
"appointment_sid": "6fe5051b-f1cd-987a-2243-b2d89d897843",
"appointment_start_time": "10:00",
"appointment_type": "general",
"apppointment_end_time": "10:30",
"contact_id": 40,
"day_of_week": "sunday",
"deactivated_on": null,
"is_deactivated": false,
"partnership_account_id": 1,
"user_id": null
}
</pre>
</p>
"""
if rest_is_partnership and rest_partnership is not None:
# Find and get the partnership account
if rest_partnership.partner_type == 'system':
partnership_account = PartnershipAccount \
.query.filter(PartnershipAccount.id == paid).first()
else:
partnership_account = PartnershipAccount.query \
.filter(and_(PartnershipAccount.id == paid,
PartnershipAccount.partnership_id == rest_partnership.id)).first()
if partnership_account is not None:
appointment = Appointment.query.filter(Appointment.sid == appsid).first()
if appointment is not None:
appointment_slot = AppointmentSlot.query\
.filter(AppointmentSlot.appointment_id == appointment.id).first()
format_date = str(appointment.appointment_date)[:-9]
weekday = week_day((appointment.appointment_date.weekday() + 1) % 7)
return jsonify(
appointment_sid=appointment.sid,
appointment_date=format_date,
day_of_week=weekday,
appointment_start_time=appointment_slot.start_time,
apppointment_end_time=appointment_slot.end_time,
appointment_type=appointment.appointment_type,
contact_id=appointment.contact_id,
partnership_account_id=appointment.partnership_account_id,
user_id=appointment.user_id,
is_deactivated=appointment.is_deactivated,
deactivated_on=appointment.deactivated_on,
)
else:
api.abort(code=404, message="The appointment was not found.")
else:
api.abort(code=404, message="Error retrieving the appointment. Partnership account not found.")
else:
api.abort(code=401)