From 2e6672dfde1086b6bcd9e090081bfcc7658938f9 Mon Sep 17 00:00:00 2001 From: Ethan-clay03 <ethanclay2017@gmail.com> Date: Thu, 13 Feb 2025 22:57:20 +0000 Subject: [PATCH] Added models for listing_availability and bookings. Completed the checkout post method, need to add receipt generation for user to be able to download --- app/bookings/routes.py | 130 +++++++++++++++++++-------- app/models/__init__.py | 4 +- app/models/bookings.py | 32 +++++++ app/models/listing_availability.py | 67 ++++++++++++++ app/templates/base.html | 1 - app/templates/bookings/checkout.html | 37 +++++--- app/templates/bookings/listing.html | 2 +- 7 files changed, 220 insertions(+), 53 deletions(-) create mode 100644 app/models/bookings.py create mode 100644 app/models/listing_availability.py diff --git a/app/bookings/routes.py b/app/bookings/routes.py index a0b36cf..e5a2bf0 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -1,6 +1,6 @@ -from flask import render_template, redirect, url_for, request, jsonify, session, flash +from flask import render_template, redirect, url_for, request, jsonify, session, flash, g from app.bookings import bp -from app.models import Listings +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 @@ -11,7 +11,7 @@ from app import user_permission, permission_required @bp.route('/') def redirect_index(): - return redirect(url_for('bookings.index'), code=301) + return redirect(url_for('bookings.listings'), code=301) @bp.route('/listings') def listings(): @@ -97,27 +97,47 @@ def listing_apply_update(): @bp.route('/checkout') -def checkout(): - if not session['checkout_cache']: +@permission_required(user_permission) +def checkout(): + if not session.get('checkout_cache'): flash("Please select a booking", 'error') - depart_date = session['checkout_cache']['depart_date'] - num_seats = session['checkout_cache']['num_seats'] - listing_id = session['checkout_cache']['listing_id'] + return redirect(url_for('bookings.listings')) - session['checkout_cache'] = { - 'depart_date': depart_date, - 'num_seats': num_seats, - 'listing_id': listing_id - } + cache = session['checkout_cache'] + depart_date = cache['depart_date'] + num_seats = int(cache['num_seats']) + listing_id = cache['listing_id'] + seat_type = cache['seat_type'] listing = Listings.search_listing(listing_id) listing.depart_time = pretty_time(listing.depart_time) listing.destination_time = pretty_time(listing.destination_time) + + per_person_excluding_discount = listing.business_fair_cost if seat_type == 'business' else listing.economy_fair_cost + discount_percentage, days_away = calculate_discount(depart_date) + discount_amount = (per_person_excluding_discount * discount_percentage) / 100 + per_person_cost = per_person_excluding_discount - discount_amount + + listing.per_person_cost = per_person_cost + listing.total_cost = per_person_cost * num_seats + total_savings = discount_amount * num_seats + + # Add per_person_cost to checkout_cache in session + session['checkout_cache']['per_person_cost'] = per_person_cost + depart_date_obj = datetime.strptime(depart_date, '%Y-%m-%d') depart_date_formatted = depart_date_obj.strftime('%d-%m-%Y') - return render_template('bookings/checkout.html', listing=listing, num_seats=num_seats, depart_date=depart_date_formatted) + return render_template( + 'bookings/checkout.html', + listing=listing, + num_seats=num_seats, + depart_date=depart_date_formatted, + seat_type=seat_type.capitalize(), + total_savings=total_savings + ) + @bp.route('/payment') @permission_required(user_permission) @@ -125,8 +145,9 @@ def payment(): depart_date = request.args.get('date') num_seats = request.args.get('seats') listing_id = request.args.get('listing_id') + seat_type = request.args.get('seat_type') - if not depart_date or not num_seats or not listing_id: + if not depart_date or not num_seats or not listing_id or not seat_type: flash('Please select a booking in order to checkout.', 'error') return redirect(url_for('bookings.listings')) @@ -134,7 +155,8 @@ def payment(): 'listing_id': listing_id, 'depart_date': depart_date, 'num_seats': num_seats, - 'listing_id': listing_id + 'listing_id': listing_id, + 'seat_type': seat_type } return redirect(url_for('bookings.checkout')) @@ -186,36 +208,72 @@ def listing(id): total_cost=total_cost ) - @bp.route('/checkout_post', methods=['POST']) +@permission_required(user_permission) def checkout_post(): card_number = request.form['cardNumber'] card_expiry = request.form['cardExpiry'] card_cvc = request.form['cardCVC'] - # Validate and process payment (pseudo-code) - if not validate_payment(card_number, card_expiry, card_cvc): - flash('Payment failed. Please check your card details.') - return redirect(url_for('checkout')) # Redirect to the checkout page on failure + valid_payment_details, payment_error_message = validate_payment(card_number, card_expiry, card_cvc) + if not valid_payment_details: + flash(payment_error_message, 'error') + return redirect(url_for('bookings.checkout')) # Assume that listing_id and user_id are obtained from session or form - listing_id = request.form['listing_id'] - user_id = request.form['user_id'] - num_seats = int(request.form['num_seats']) - - # Create booking - if create_booking(listing_id, user_id, num_seats): - # Update availability after successful booking - update_listing_availability(listing_id, num_seats) - flash('Booking successful!') - else: - flash('Booking failed. Please try again.') - - return redirect(url_for('booking_confirmation')) + cache = session['checkout_cache'] + listing_id = cache['listing_id'] + user_id = g.identity.id + num_seats = int(cache['num_seats']) + seat_type = cache['seat_type'] + per_person_cost = cache['per_person_cost'] + total_cost = per_person_cost * num_seats + + depart_date = cache['depart_date'] + + # Convert depart_date to date object + depart_date_obj = datetime.strptime(depart_date, '%Y-%m-%d').date() + + availability = ListingAvailability.check_availability(listing_id, depart_date_obj, seat_type, num_seats) + if availability != True: + flash(f"Not enough seats available. There are {availability} remaining seats for {seat_type.capitalize()}.", 'error') + return redirect(url_for('bookings.listing', id=listing_id)) + + try: + if Bookings.create_booking(listing_id, user_id, total_cost, seat_type, num_seats): + # Update availability + ListingAvailability.update_availability(listing_id, depart_date_obj, seat_type, num_seats) + db.session.commit() + flash('Booking successful!', 'success') + else: + flash('Booking failed. Please try again.', 'error') + return redirect(url_for('bookings.checkout')) + except Exception as e: + db.session.rollback() + error_logger.debug(f"Error processing booking: {e}") + flash('Booking failed. Please try again.', 'error') + + return redirect(url_for('bookings.listings')) + def validate_payment(card_number, card_expiry, card_cvc): - # Implement your payment validation logic here - return + if len(card_number) != 16 or not card_number.isdigit(): + return False, "Invalid card number. It must be 16 digits." + + if len(card_cvc) != 3 or not card_cvc.isdigit(): + return False, "Invalid CVC. It must be 3 digits." + + try: + exp_month, exp_year = card_expiry.split('/') + exp_month = int(exp_month) + exp_year = int(exp_year) + 2000 + expiry_date = datetime(exp_year, exp_month, 1) + if expiry_date < datetime.now(): + return False, "Card expiry date cannot be in the past." + except ValueError: + return False, "Invalid expiry date format. It must be MM/YY." + + return True, 'Success' @bp.route('/filter_bookings', methods=['POST']) def filter_bookings(): diff --git a/app/models/__init__.py b/app/models/__init__.py index de002ee..2b9d0bd 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -2,4 +2,6 @@ from .user import User from .listings import Listings from .listing_images import ListingImages -from .role import Role \ No newline at end of file +from .role import Role +from .bookings import Bookings +from .listing_availability import ListingAvailability \ No newline at end of file diff --git a/app/models/bookings.py b/app/models/bookings.py new file mode 100644 index 0000000..351f02a --- /dev/null +++ b/app/models/bookings.py @@ -0,0 +1,32 @@ +from app import db +from flask_login import UserMixin + +class Bookings(UserMixin, db.Model): + __tablename__ = 'bookings' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, nullable=False) + listing_id = db.Column(db.Integer, nullable=False) + amount_paid = db.Column(db.Float, nullable=False) + seat_type = db.Column(db.String(50), nullable=False) + num_seats = db.Column(db.Integer, nullable=False) + cancelled = db.Column(db.Boolean, default=False) + + @staticmethod + def create_booking(listing_id, user_id, amount_paid, seat_type, num_seats): + try: + new_booking = Bookings( + user_id=user_id, + listing_id=listing_id, + amount_paid=amount_paid, + seat_type=seat_type, + num_seats=num_seats, + cancelled=False + ) + db.session.add(new_booking) + db.session.commit() + return True + except Exception as e: + db.session.rollback() + print(f"Error creating booking: {e}") + return False diff --git a/app/models/listing_availability.py b/app/models/listing_availability.py new file mode 100644 index 0000000..8916b4e --- /dev/null +++ b/app/models/listing_availability.py @@ -0,0 +1,67 @@ +from app import db + +class ListingAvailability(db.Model): + __tablename__ = 'listing_availability' + + id = db.Column(db.Integer, primary_key=True) + listing_id = db.Column(db.Integer, nullable=False) + date = db.Column(db.Date, nullable=False) + air_economy_seats = db.Column(db.Integer, nullable=False) + air_business_seats = db.Column(db.Integer, nullable=False) + + @staticmethod + def check_availability(listing_id, date, seat_type, num_seats): + availability = ListingAvailability.query.filter_by( + listing_id=listing_id, + date=date + ).first() + + if availability == None: + ListingAvailability.create_availability(listing_id, date) + + # Fetch the newly created availability row + availability = ListingAvailability.query.filter_by( + listing_id=listing_id, + date=date + ).first() + + if seat_type == 'business': + if availability.air_business_seats >= num_seats: + return True + else: + return availability.air_business_seats + else: + if availability.air_economy_seats >= num_seats: + return True + else: + return availability.air_economy_seats + + @staticmethod + def update_availability(listing_id, date, seat_type, num_seats): + availability = ListingAvailability.query.filter_by( + listing_id=listing_id, + date=date + ).first() + if availability: + if seat_type == 'business': + availability.air_business_seats -= num_seats + else: + availability.air_economy_seats -= num_seats + db.session.commit() + + @staticmethod + def create_availability(listing_id, date, economy_seats = 104, business_seats = 26): + try: + new_availability = ListingAvailability( + listing_id=listing_id, + date=date, + air_economy_seats=economy_seats, # Defaults as set per the spec, calling create_availibility you can override the values + air_business_seats=business_seats + ) + db.session.add(new_availability) + db.session.commit() + return new_availability + except Exception as e: + db.session.rollback() + print(f"Error creating availability: {e}") + return None diff --git a/app/templates/base.html b/app/templates/base.html index 5e00943..5aee9d1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,7 +1,6 @@ <!DOCTYPE html> <html lang="en"> <head> - <!-- Existing dependencies --> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="csrf-token" content="{{ csrf_token() }}"> diff --git a/app/templates/bookings/checkout.html b/app/templates/bookings/checkout.html index ec3cff6..99f6372 100644 --- a/app/templates/bookings/checkout.html +++ b/app/templates/bookings/checkout.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% block content %} +<script src="{{ url_for('static', filename='generic.js') }}"></script> <div class="container mt-5"> <h2 class="mb-4" style="text-align: center; margin-top: 40px;">Booking Details</h2> <div class="row"> @@ -7,13 +8,22 @@ <div class="card mb-4"> <div class="card-body"> <h2 class="card-title">Summary</h2> - <p><strong>Departure Date:</strong> {{ depart_date }}</p> - <p><strong>Number of Seats:</strong> {{ num_seats }}</p> - <p><strong>Departure Location:</strong> {{ listing.depart_location }}</p> - <p><strong>Departure Time:</strong> {{ listing.depart_time }}</p> - <p><strong>Destination Location:</strong> {{ listing.destination_location }}</p> - <p><strong>Destination Time:</strong> {{ listing.destination_time }}</p> - <p><strong>Total Cost:</strong> £{{ listing.fair_cost * num_seats|int }}</p> + <div class="row"> + <div class="col-md-6"> + <p><strong>Departure Date:</strong> {{ depart_date }}</p> + <p><strong>Departure Location:</strong> {{ listing.depart_location }}</p> + <p><strong>Departure Time:</strong> {{ listing.depart_time }}</p> + <p><strong>Seat Type:</strong> {{ seat_type }}</p> + <p><strong>Total Cost:</strong> £{{ listing.total_cost }}</p> + </div> + <div class="col-md-6"> + <p><strong>Destination Location:</strong> {{ listing.destination_location }}</p> + <p><strong>Destination Time:</strong> {{ listing.destination_time }}</p> + <p><strong>Number of Seats:</strong> {{ num_seats }}</p> + <p><strong>Cost Per Person:</strong> £{{ listing.per_person_cost }}</p> + <p><strong>Total Saving:</strong> £{{ total_savings }}</p> + </div> + </div> </div> </div> </div> @@ -25,6 +35,7 @@ <div class="card-body"> <h2 class="card-title">Payment Information</h2> <form id="paymentForm" action="{{ url_for('bookings.checkout_post') }}" method="post"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <div class="form-group"> <label for="cardNumber">Credit Card Number</label> <input type="text" class="form-control" id="cardNumber" name="cardNumber" placeholder="Enter your credit card number"> @@ -54,7 +65,7 @@ pristine.addValidator( document.getElementById('cardNumber'), function (value) { - return /^[0-9]{16}$/.test(value); // Check if the card number is 16 digits + return /^[0-9]{16}$/.test(value); //Regex 16 number check }, "Credit card number must be 16 digits", 2, @@ -67,7 +78,7 @@ var parts = value.split('/'); if (parts.length !== 2) return false; var month = parseInt(parts[0], 10); - var year = parseInt(parts[1], 10) + 2000; // Assuming the year is in YY format + var year = parseInt(parts[1], 10) + 2000; var expiryDate = new Date(year, month - 1); return expiryDate >= new Date(); }, @@ -79,7 +90,7 @@ pristine.addValidator( document.getElementById('cardCVC'), function (value) { - return /^[0-9]{3}$/.test(value); // Check if the CVC is 3 digits + return /^[0-9]{3}$/.test(value); // Regex 3 digit check }, "CVC must be 3 digits", 2, @@ -88,15 +99,14 @@ form.addEventListener('submit', function (e) { e.preventDefault(); - var valid = pristine.validate(); // returns true or false + var valid = pristine.validate(); if (valid) { - alert('Payment information is valid!'); form.submit(); } }); - // Validate on blur (focus lost) + // Validate on lost focus var inputs = form.querySelectorAll('input'); inputs.forEach(function (input) { input.addEventListener('blur', function () { @@ -110,5 +120,4 @@ document.head.appendChild(style); }); </script> - {% endblock %} diff --git a/app/templates/bookings/listing.html b/app/templates/bookings/listing.html index 087b326..f165b55 100644 --- a/app/templates/bookings/listing.html +++ b/app/templates/bookings/listing.html @@ -184,7 +184,7 @@ const seatType = seatTypeInput.value; const listingId = "{{listing.id}}"; - window.location.href = `{{ url_for('bookings.payment') }}?date=${departDate}&seats=${numSeats}&listing_id=${listingId}`; + window.location.href = `{{ url_for('bookings.payment') }}?date=${departDate}&seats=${numSeats}&listing_id=${listingId}&seat_type=${seatType}`; }); }); </script> -- GitLab