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