diff --git a/app/__init__.py b/app/__init__.py index 8cbfb643751a3a97067028c7d1476640f0fd4a38..6d7ee36c7dc9768770cc9c97b688b396ef806b11 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -161,7 +161,7 @@ def create_app(config_class=Config): def handle_exception(e): app.logger.error(f"Unhandled exception: {e}") session['error_message'] = str(e) - return redirect(url_for('errors.quandary')) + return redirect(url_for('errors.error')) @app.errorhandler(403) def handle_exception(e): diff --git a/app/bookings/routes.py b/app/bookings/routes.py index f439d53e9fc9062efc436dc3c0cfadb8671f6790..b839799f62d11f2972337d27c77a38570031f9f2 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -3,7 +3,7 @@ from app.bookings import bp from app.models import Listings, Bookings, ListingAvailability from app import db from app.logger import error_logger -from app.main.utils import calculate_discount, pretty_time, create_receipt, create_plane_ticket +from app.main.utils import calculate_discount, pretty_time, create_receipt, create_plane_ticket, calculate_refund_amount import json from datetime import datetime from app import user_permission, permission_required @@ -424,3 +424,21 @@ def get_user_bookings(): ] return jsonify(result) + + +@bp.route('/cancel_booking/<int:id>', methods=['POST']) +@permission_required(user_permission) +def cancel_booking(id): + if not id: + flash('Unable to cancel booking', 'error') + return redirect(url_for('bookings.manage_bookings')) + + booking = Bookings.search_booking(id) + cancel_amount, cancel_percentage = calculate_refund_amount(booking) + success = Bookings.cancel_booking(id, cancel_amount) + if success: + flash('Your booking has been successfully cancelled. If you are entitled to a refund this will be refunded to your payment card', 'success') + return redirect(url_for('profile.manage_bookings')) + else: + flash('Unable to cancel booking, please try again', 'error') + return redirect(url_for('profile.manage_profile_view_booking', id=id)) \ No newline at end of file diff --git a/app/errors/routes.py b/app/errors/routes.py index a918481d9c8e49745fc009c78e4505b743b313ad..981d67ec95d9c32a6d07299067079cffb88bf7fd 100644 --- a/app/errors/routes.py +++ b/app/errors/routes.py @@ -1,14 +1,16 @@ -from flask import render_template, session +from flask import render_template, session, g, current_app from app.errors import bp +from app.logger import error_logger -@bp.route('/quandary') -def quandary(): +@bp.route('/error') +def error(): error_message = 'Something went wrong, if this continues please contact support.' - if 'error_message' in session: - error_message = session.pop('error_message') - return render_template("errors/quandary.html", error_message=error_message) + if g.is_admin: # Only display error if admin is logged in, otherwise throw generic error to user + error_message = session.get('error_message') + error_logger.error(f"Error: {error_message} \nAction performed by UserID: {session['_user_id']}") + return render_template("errors/error.html", error_message=error_message) @bp.route('/no_permission') def no_permission(): error_message = 'You do not have the required permission to view this page.' - return render_template("errors/quandary.html", error_message=error_message) + return render_template("errors/error.html", error_message=error_message) diff --git a/app/main/utils.py b/app/main/utils.py index fb372472404b694a3775b3e6ef0f39bda6686d42..9aa91c9e6ec889361f73d43ac1f1a276650c71a0 100644 --- a/app/main/utils.py +++ b/app/main/utils.py @@ -1,7 +1,7 @@ # utils.py from flask import current_app -from datetime import time, datetime +from datetime import time, datetime, date from datetime import datetime from fpdf import FPDF import barcode @@ -10,7 +10,7 @@ from PyPDF2 import PdfMerger from io import BytesIO import os from PIL import Image -from pystrich.datamatrix import DataMatrixEncoder, DataMatrixRenderer +from pystrich.datamatrix import DataMatrixEncoder def allowed_image_files(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS'] @@ -39,22 +39,20 @@ def calculate_discount(date): return 0, days_away -def calculate_refund_amount(amount_paid): - depart_date = datetime.strptime(date, '%Y-%m-%d') - today = datetime.now() - days_away = (depart_date - today).days +def calculate_refund_amount(booking): + days_until_departure = get_days_until_departure(booking) - if 80 <= days_away <= 90: - return 25, days_away - elif 60 <= days_away <= 79: - return 15, days_away - elif 45 <= days_away <= 59: - return 10, days_away + if days_until_departure < 30: + return 0, 0 # 0% + elif 30 <= days_until_departure < 60: + return booking.amount_paid * 0.6, 60 # 60% refund else: - return 0, days_away + return booking.amount_paid, 100 # 100% refund def pretty_time(unformatted_time, to_12_hour=True): + if unformatted_time == None: + return None if not isinstance(unformatted_time, (datetime, time)): unformatted_time = datetime.strptime(unformatted_time, "%H:%M:%S") @@ -66,19 +64,13 @@ def pretty_time(unformatted_time, to_12_hour=True): return formatted_time - +def get_days_until_departure(booking): + today = datetime.now().date() + return (booking.depart_date - today).days def get_static_files(*path_parts): return os.path.join(current_app.root_path, 'static', *path_parts) -import os -import tempfile -from PyPDF2 import PdfMerger -from PIL import Image -import barcode -from barcode.writer import ImageWriter -from pystrich.datamatrix import DataMatrixEncoder, DataMatrixRenderer - def create_receipt(booking, listing): formatted_depart_date = booking.depart_date.strftime('%d-%m-%Y') formatted_booking_date = booking.booking_date.strftime('%d-%m-%Y') diff --git a/app/models/bookings.py b/app/models/bookings.py index d7ba805f1a6c2f525893dea049c3ed650a32bf53..7c9665dbb160d5c0cb3d8460ad094d08eba29ae5 100644 --- a/app/models/bookings.py +++ b/app/models/bookings.py @@ -15,6 +15,7 @@ class Bookings(UserMixin, db.Model): num_seats = db.Column(db.Integer, nullable=False) cancelled = db.Column(db.Boolean, default=False) cancelled_date = db.Column(db.Date, nullable=False) + refund_amount = db.Column(db.Float, nullable=False) booking_date = db.Column(db.Date, nullable=False) depart_date = db.Column(db.Date, nullable=False) last_four_card_nums = db.Column(db.String(4), nullable=False) @@ -61,11 +62,12 @@ class Bookings(UserMixin, db.Model): return booking is not None @classmethod - def cancel_booking(cls, booking_id): + def cancel_booking(cls, booking_id, refund_amount = 0): booking = cls.query.get(booking_id) if booking: booking.cancelled = True booking.cancelled_date = datetime.utcnow().date() + booking.refund_amount = refund_amount db.session.commit() return True return False diff --git a/app/profile/routes.py b/app/profile/routes.py index 614e54898e49124020eeceace76cf0bc6d377526..1fda3ba49119ba604dd7e81d4a92cafb14716363 100644 --- a/app/profile/routes.py +++ b/app/profile/routes.py @@ -4,7 +4,7 @@ from flask_principal import Identity, identity_changed from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash from app.profile import bp -from app.main.utils import pretty_time, calculate_refund_amount +from app.main.utils import pretty_time, calculate_refund_amount, get_days_until_departure from app.models import User, Bookings, Listings from app.logger import auth_logger from app import db, permission_required, user_permission @@ -260,33 +260,22 @@ def manage_profile(): return render_template('profile/manage_profile.html', user=user) -@bp.route('/cancel_booking', methods=['POST']) -@permission_required(user_permission) -def cancel_booking(): - data = request.get_json() - booking_id = data.get('booking_id') - if not booking_id: - return jsonify({'error': 'Missing booking_id'}), 400 - - success = Bookings.cancel_booking(booking_id) - if success: - return jsonify({'message': 'Booking cancelled successfully'}), 200 - else: - return jsonify({'error': 'Failed to cancel booking'}), 400 - - @bp.route('/manage_bookings/view/<int:id>') def manage_profile_view_booking(id): booking = Bookings.search_booking(id) booking.listing.destination_time = pretty_time(booking.listing.destination_time) booking.listing.depart_time = pretty_time(booking.listing.depart_time) - booking.cancelled_date = pretty_time(booking.cancelled_date) - cancel_amount, cancel_percentage = calculate_refund_amount() + days_until_departure = get_days_until_departure(booking) + cancel_amount, cancel_percentage = calculate_refund_amount(booking) refund = { - amount: - percentage: + 'amount': cancel_amount, + 'percentage': cancel_percentage } + + departed = False + if days_until_departure < 0: + departed = True - return render_template('profile/view_booking.html', booking=booking) + return render_template('profile/view_booking.html', booking=booking, refund=refund, days_until_departure=days_until_departure, departed=departed) diff --git a/app/templates/errors/quandary.html b/app/templates/errors/error.html similarity index 99% rename from app/templates/errors/quandary.html rename to app/templates/errors/error.html index afa71393a9374ea052920fb06e81f87f707393ba..fda2e0fba6ce7b2d063bcc832cf44de59966f498 100644 --- a/app/templates/errors/quandary.html +++ b/app/templates/errors/error.html @@ -17,7 +17,6 @@ </div> </div> </div> - <style> .quandary-div { height: 100vh; diff --git a/app/templates/profile/manage_profile.html b/app/templates/profile/manage_profile.html index 054dea26da679cf166afec2e68ed8ed5f44b4eb4..4b47004a87cf750a4c5e4701b709301bd9d22ef9 100644 --- a/app/templates/profile/manage_profile.html +++ b/app/templates/profile/manage_profile.html @@ -21,7 +21,6 @@ <a href="{{ url_for('profile.password_reset_process') }}" class="btn btn-warning ml-2"><i class="fas fa-key"></i> Reset Password</a> </div> </div> - <!-- Modal --> <div class="modal fade" id="confirmModal" tabindex="-1" role="dialog" aria-labelledby="confirmModalLabel" aria-hidden="true"> <div class="modal-dialog" role="document"> @@ -41,7 +40,6 @@ </div> </div> </div> - <script> function enableEditing() { document.getElementById('username').disabled = false; diff --git a/app/templates/profile/view_booking.html b/app/templates/profile/view_booking.html index c0e36ca55966e9effd2f26c0db3694088c5e67f2..29ee6a99a707b2fd7ce4ef83046108a2a8722135 100644 --- a/app/templates/profile/view_booking.html +++ b/app/templates/profile/view_booking.html @@ -16,7 +16,7 @@ <p><strong>Seat Type:</strong> {{ booking.seat_type.capitalize() }}</p> <p><strong>Total Cost:</strong> £{{ booking.amount_paid }}</p> {% if booking.cancelled %} - <p><strong>Cancellation Date:</strong> £{{ booking.cancelled_date }}</p> + <p><strong>Cancellation Date:</strong> {{ booking.cancelled_date }}</p> {% endif %} </div> <div class="col-md-6"> @@ -26,21 +26,34 @@ <p><strong>Cost Per Person:</strong> £{{ booking.amount_paid / booking.num_seats }}</p> <p><strong>Cancelled:</strong> {{ 'Yes' if booking.cancelled else 'No' }}</p> {% if booking.cancelled %} - <p><strong>Refunded Amount:</strong> £{{ booking.cancelled_refund }}</p> + <p><strong>Refunded Amount:</strong> £{{booking.refund_amount if booking.refund_amount else '0'}}</p> {% endif %} </div> </div> </div> </div> + <div class="card mb-4 shadow-sm"> <div class="card-body text-center"> <h3 class="card-title mb-4">Re-Download Booking Details</h3> + {% if booking.cancelled %} <div class="alert alert-danger" role="alert"> - This booking has been cancelled. Download options are no longer available + This booking has been cancelled. Download options are no longer available. </div> <button type="button" class="btn btn-success btn-lg mr-2" disabled>Download Receipt</button> <button type="button" class="btn btn-primary btn-lg" disabled>Download Plane Ticket</button> + + {% elif departed == True %} + <div class="alert alert-warning" role="alert"> + This booking has already taken place. + </div> + <div class="d-flex justify-content-center"> + <form action="{{ url_for('bookings.generate_receipt', id=booking.id) }}" method="get" class="d-inline"> + <button type="submit" class="btn btn-success btn-lg mr-2">Download Receipt</button> + </form> + </div> + {% else %} <div class="d-flex justify-content-center"> <form action="{{ url_for('bookings.generate_receipt', id=booking.id) }}" method="get" class="d-inline"> @@ -53,17 +66,27 @@ {% endif %} </div> </div> - {% if not booking.cancelled %} - <div class="card shadow-sm"> - <div class="card-body text-center"> - <h3 class="card-title mb-4">Cancel Booking</h3> + + {% if not booking.cancelled and not departed == True %} + <div class="card shadow-sm"> + <div class="card-body text-center"> + <h3 class="card-title mb-4">Cancel Booking</h3> <div class="alert alert-info" role="alert"> - Need to cancel? Not a problem as you are x days away you are entitled to a x% refund! Your refunded amount will be £xx.x which - will automatically be refunded to your card ending in 1234 + {% if refund['percentage'] == 0 %} + Cancelling now will not provide a refund as you are within the non-refundable period. + {% else %} + Need to cancel? Since you are {{ days_until_departure }} days away, you are entitled to a {{ refund['percentage'] }}% refund! + Your refunded amount will be £{{ refund['amount'] }}, which will automatically be refunded to your card ending in {{ booking.last_four_card_nums }}. + {% endif %} </div> - <button type="button" class="btn btn-danger btn-lg mr-2">Cancel Booking</button> + <form action="{{ url_for('bookings.cancel_booking', id=booking.id) }}" method="POST" onsubmit="return confirm('Are you sure you want to cancel this booking?');"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <button type="submit" class="btn btn-danger btn-lg mr-2"> + Cancel Booking + </button> + </form> + </div> </div> - </div> {% endif %} </div> </div>