import os from flask import Flask, render_template, request, redirect, url_for, flash, abort, send_from_directory, jsonify from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_mail import Mail, Message from datetime import datetime from dotenv import load_dotenv from tours_data import TourData from destination_data import DestinationData from flask import send_from_directory import logging load_dotenv() # Load environment variables from .env app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['PREFERRED_URL_SCHEME'] = 'https' # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # PayPal Configuration PAYPAL_MODE = os.getenv('PAYPAL_MODE', 'live') # Changed default to 'live' PAYPAL_API_BASE_URL = ( 'https://api-m.sandbox.paypal.com' if PAYPAL_MODE == 'sandbox' else 'https://api-m.paypal.com' ) logger.info(f"PayPal API Base URL: {PAYPAL_API_BASE_URL}") logger.info(f"PayPal Mode: {PAYPAL_MODE}") # Add logging for PayPal mode #if PAYPAL_MODE == 'live': app.config['PAYPAL_CLIENT_ID'] = os.getenv('PAYPAL_CLIENT_ID_LIVE') app.config['PAYPAL_SECRET'] = os.getenv('PAYPAL_SECRET_LIVE') logger.info("Using PayPal Live Mode") #else: # app.config['PAYPAL_CLIENT_ID'] = os.getenv('PAYPAL_CLIENT_ID_SANDBOX') # app.config['PAYPAL_SECRET'] = os.getenv('PAYPAL_SECRET_SANDBOX') # logger.warning("Using PayPal Sandbox Mode") # Initialize SQLAlchemy db = SQLAlchemy(app) # Initialize Flask-Migrate migrate = Migrate(app, db) # Mail Configuration app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER') app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT')) app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS') == 'true' app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME') app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD') mail = Mail(app) # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @app.context_processor def utility_processor(): def current_year(): return datetime.now().year return dict(current_year=current_year) @app.context_processor def inject_contact_info(): contact_info = { 'EMAIL': os.getenv('EMAIL'), 'WHATSAPP_NUMBER': os.getenv('WHATSAPP_NUMBER'), 'ADDRESS': os.getenv('ADDRESS') } return dict(contact_info=contact_info) @app.context_processor def inject_paypal_client_id(): return { 'PAYPAL_CLIENT_ID_LIVE': os.getenv('PAYPAL_CLIENT_ID_LIVE'), 'PAYPAL_ID': os.getenv('PAYPAL_CLIENT_ID_LIVE') # Ensure we're using live ID } class Tour(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) description = db.Column(db.Text, nullable=False) price = db.Column(db.Float, nullable=False) duration = db.Column(db.Integer, nullable=False) destination = db.Column(db.String(100), nullable=False) category = db.Column(db.String(50), nullable=False) highlights = db.Column(db.Text) itinerary = db.Column(db.Text) image_url = db.Column(db.String(200)) slug = db.Column(db.String(100), nullable=True) class Booking(db.Model): id = db.Column(db.Integer, primary_key=True) tour_id = db.Column(db.Integer, db.ForeignKey('tour.id'), nullable=False) name = db.Column(db.String(100), nullable=False) email = db.Column(db.String(100), nullable=False) phone = db.Column(db.String(20), nullable=False) message = db.Column(db.Text) travelers = db.Column(db.Integer, nullable=False) # New field for number of travelers booking_date = db.Column(db.DateTime, default=datetime.utcnow) tours_data = TourData() @app.route('/') def index(): import logging logger = logging.getLogger(__name__) # Get destinations from DestinationData all_destinations = DestinationData.get_destinations() # Prepare featured destinations for carousel featured_destinations = [] for slug, destination in all_destinations.items(): # Use the first 150 characters of description or meta description short_description = ( destination.get('description', '') or destination.get('meta_description', '') )[:150] + '...' # Create a carousel-friendly destination object carousel_destination = { 'name': destination.get('name', destination.get('slug', '').title()), 'slug': slug, 'image': destination.get('image', url_for('static', filename=f'images/destinations/{slug}-hero.jpg')), 'description': short_description, 'tagline': destination.get('tagline', '') } featured_destinations.append(carousel_destination) # Limit to first 6 destinations featured_destinations = featured_destinations[:6] # Get featured tours tours = tours_data.get_all_tours() logger.info(f"Total tours available: {len(tours)}") # Handle both list and dictionary tour data formats if isinstance(tours, dict): featured_tours = list(tours.values())[:3] logger.info(f"Available tour slugs: {list(tours.keys())}") else: featured_tours = tours[:3] if tours else [] logger.info(f"Available tours count: {len(tours)}") meta = { 'title': 'Fez Guided Tour - Unforgettable Moroccan Adventures', 'description': 'Discover the magic of Fez and Morocco with our expertly crafted guided tours.', 'keywords': 'Fez, Morocco, Guided Tours, Travel', 'canonical': url_for('index', _external=True) } return render_template('index.html', meta=meta, featured_destinations=featured_destinations, featured_tours=featured_tours) @app.route('/destinations') def destinations(): # Get all destinations all_destinations = DestinationData.get_destinations() # Convert dictionary to list and sort if needed destinations_list = list(all_destinations.values()) # Optional: Add any additional sorting or filtering logic here return render_template('destinations.html', destinations=destinations_list, total_destinations=len(destinations_list)) @app.route('/book_tour', methods=['GET', 'POST']) def book_tour(): # Get the tour_id or slug from the request tour_id = request.args.get('tour_id') tour_slug = request.args.get('slug') if not tour_id and not tour_slug: flash('No tour selected', 'error') return redirect(url_for('tours')) # Try to fetch tour from database first tour = None if tour_id: tour = Tour.query.get(tour_id) else: tour = Tour.query.filter_by(slug=tour_slug).first() # If not found in database, try to get from TourData if not tour and tour_slug: tour_data = tours_data.get_tour_details(tour_slug) if tour_data: # Create a temporary tour-like object class TempTour: def __init__(self, tour_data): self.id = tour_data.get('id', 0) self.name = tour_data.get('name', '') self.description = tour_data.get('description', '') self.price = tour_data.get('price', 0) self.image_url = tour_data.get('image', '') or tour_data.get('hero_image', '') self.duration = tour_data.get('duration', '') self.slug = tour_data.get('slug', '') # Add this line tour = TempTour(tour_data) if not tour: flash('Tour not found', 'error') return redirect(url_for('tours')) if request.method == 'POST': # Process booking form submission name = request.form.get('name') email = request.form.get('email') phone = request.form.get('phone') message = request.form.get('message') travelers = request.form.get('travelers') travel_date = request.form.get('travel_date') if not name or not phone or not travelers or not travel_date: flash('Please fill in all required fields.', 'error') return redirect(url_for('book_tour', slug=tour_slug)) # Create a new booking new_booking = Booking( tour_id=tour.id, name=name, email=email, phone=phone, message=message ) try: db.session.add(new_booking) db.session.commit() flash('Booking submitted successfully!', 'success') return redirect(url_for('tours')) except Exception as e: db.session.rollback() flash(f'Error submitting booking: {str(e)}', 'error') return render_template('book_tour.html', tour=tour) @app.route('/book_tour/', methods=['GET', 'POST']) def book_tour_slug(slug): # Ensure the slug is exactly as defined in tours_data.py valid_slugs = [ 'imperial-cities-classic', 'fez-marrakech-express', 'sahara-desert-adventure', 'atlantic-coast-journey' ] if slug not in valid_slugs: flash('Tour not found', 'error') return redirect(url_for('tours')) tour = tours_data.get_tour_details(slug) # Extract the number of days from the duration string duration_parts = tour['duration'].split() tour['duration_days'] = int(duration_parts[0]) # Add the slug to the tour dictionary tour['slug'] = slug if request.method == 'POST': # Process booking form submission name = request.form.get('name') email = request.form.get('email') phone = request.form.get('phone') message = request.form.get('message') # Create a new booking new_booking = Booking( tour_id=tour['id'] if 'id' in tour else None, name=name, email=email, phone=phone, message=message ) try: db.session.add(new_booking) db.session.commit() flash('Booking submitted successfully!', 'success') return redirect(url_for('tours')) except Exception as e: db.session.rollback() flash(f'Error submitting booking: {str(e)}', 'error') return render_template('book_tour.html', tour=tour) @app.route('/about') def about(): return render_template('about.html') @app.route('/contact') def contact(): contact_info = { 'EMAIL': os.getenv('EMAIL'), 'WHATSAPP_NUMBER': os.getenv('WHATSAPP_NUMBER'), 'ADDRESS': os.getenv('ADDRESS') } return render_template('contact.html', config=contact_info) @app.route('/tours') def tours(): tours = [ { 'category': 'Grand Tours', 'tours': [ { 'name': 'Moroccan Grand Tour: 17-Day Ultimate Exploration', 'slug': 'moroccan-grand-tour', 'image': '/static/images/tours/moroccan-grand-tour.jpg', 'description': 'The most comprehensive journey through Morocco, exploring diverse landscapes, rich cultures, and historic wonders from bustling imperial cities to serene desert landscapes.', 'price': 1850, 'duration': '17' }, { 'name': 'Morocco Highlights: 12-Day Comprehensive Tour', 'slug': 'morocco-highlights-12-days', 'image': '/static/images/tours/morocco-highlights-12-days.jpg', 'description': 'A comprehensive journey through Morocco\'s most iconic destinations, blending cultural experiences, historical sites, and natural wonders.', 'price': 1250, 'duration': '12' }, { 'name': 'Morocco Discovery: 11-Day Cultural Journey', 'slug': 'morocco-discovery-11-days', 'image': '/static/images/tours/morocco-discovery-11-days.jpg', 'description': 'An immersive tour covering Morocco\'s most iconic destinations and cultural experiences, offering a deep dive into the country\'s rich heritage.', 'price': 1100, 'duration': '11' }, { 'name': 'Moroccan Essence: 10-Day Expedition', 'slug': 'moroccan-essence-10-days', 'image': '/static/images/tours/moroccan-essence-10-days.jpg', 'description': 'A carefully curated journey through Morocco\'s most stunning landscapes and vibrant cities, capturing the essence of Moroccan culture.', 'price': 1000, 'duration': '10' }, { 'name': 'Morocco Explorer: 9-Day Adventure', 'slug': 'morocco-explorer-9-days', 'image': '/static/images/tours/morocco-explorer-9-days.jpg', 'description': 'A compact yet comprehensive tour showcasing the diverse beauty of Morocco, perfect for travelers with limited time.', 'price': 900, 'duration': '9' }, { 'name': 'Morocco Highlights: 8-Day Classic Tour', 'slug': 'morocco-highlights-8-days', 'image': '/static/images/tours/morocco-highlights-8-days.jpg', 'description': 'An 8-day journey through Morocco\'s most iconic destinations, offering a perfect introduction to the country\'s rich culture and landscapes.', 'price': 800, 'duration': '8' } ] }, { 'category': 'Imperial Cities', 'tours': [ { 'name': 'Imperial Cities Classic Tour', 'slug': 'imperial-cities-classic', 'image': '/static/images/tours/imperial-cities-classic.jpg', 'description': 'Embark on an extraordinary journey through Morocco\'s most prestigious historical cities. This comprehensive tour takes you through the imperial capitals of Fez, Marrakech, Meknes, and Rabat, revealing centuries of rich cultural heritage.', 'price': 450, 'duration': '7' }, { 'name': 'Fez & Marrakech Express', 'slug': 'fez-marrakech-express', 'image': '/static/images/tours/fez-marrakech-express.jpg', 'description': 'Experience the essence of Morocco\'s most iconic cities in this intense and immersive tour. Dive deep into the labyrinthine medina of Fez and the vibrant city of Marrakech.', 'price': 350, 'duration': '4' } ] } ] return render_template('tours.html', tours=tours) @app.route('/tour/') def tour_detail(slug): tour = tours_data.get_tour_details(slug) if tour: return render_template('tour_detail.html', tour=tour) else: abort(404) @app.route('/workshops') def workshops(): workshops_data = [ { 'name': 'Ceramic Pottery Workshop', 'slug': 'ceramic-pottery', 'description': 'Learn the ancient art of Moroccan ceramic pottery in the heart of Fez', 'duration': '3 hours', 'difficulty': 'Beginner', 'price': 45, 'image': '/static/images/workshops/ceramic-pottery.jpg', 'highlights': [ 'Traditional pottery techniques', 'Work with local artisans', 'Create your own ceramic piece', 'Learn about Moroccan ceramic history' ] }, { 'name': 'Leather Crafting Class', 'slug': 'leather-crafting', 'description': 'Master the traditional Moroccan leather crafting techniques in a historic tannery', 'duration': '4 hours', 'difficulty': 'Intermediate', 'price': 65, 'image': '/static/images/workshops/leather-crafting.jpg', 'highlights': [ 'Learn leather cutting and stitching', 'Visit a traditional tannery', 'Create a personalized leather item', 'Understand leather processing techniques' ] }, { 'name': 'Moroccan Cooking Class', 'slug': 'moroccan-cooking', 'description': 'Discover the secrets of Moroccan cuisine through a hands-on cooking experience', 'duration': '5 hours', 'difficulty': 'All Levels', 'price': 55, 'image': '/static/images/workshops/moroccan-cooking.jpg', 'highlights': [ 'Market tour and ingredient selection', 'Cooking traditional tagines and couscous', 'Learn spice blending techniques', 'Enjoy a meal you prepared' ] }, { 'name': 'Calligraphy Workshop', 'slug': 'calligraphy', 'description': 'Learn the elegant art of Arabic calligraphy from a master calligrapher', 'duration': '2 hours', 'difficulty': 'Beginner', 'price': 35, 'image': '/static/images/workshops/calligraphy.jpg', 'highlights': [ 'Introduction to Arabic script styles', 'Practice with traditional tools', 'Create your own calligraphic artwork', 'Learn cultural significance of calligraphy' ] }, { 'name': 'Spice Blending Workshop', 'slug': 'spice-blending', 'description': 'Explore the world of Moroccan spices and learn to create your own unique blends', 'duration': '2.5 hours', 'difficulty': 'All Levels', 'price': 40, 'image': '/static/images/workshops/spice-blending.jpg', 'highlights': [ 'Spice identification and sourcing', 'Create personal spice blends', 'Learn about Moroccan culinary traditions', 'Take home your custom spice mix' ] }, { 'name': 'Textile Weaving Class', 'slug': 'textile-weaving', 'description': 'Learn traditional Berber textile weaving techniques passed down through generations', 'duration': '4 hours', 'difficulty': 'Intermediate', 'price': 50, 'image': '/static/images/workshops/textile-weaving.jpg', 'highlights': [ 'Traditional loom techniques', 'Work with local weavers', 'Create a small textile piece', 'Understand cultural textile significance' ] } ] return render_template('workshops.html', workshops=workshops_data) @app.route('/workshop/') def workshop_detail(slug): workshops_data = { 'ceramic-pottery': { 'name': 'Ceramic Pottery Workshop', 'slug': 'ceramic-pottery', 'description': 'A deep dive into the ancient art of Moroccan ceramic pottery in the historic city of Fez. This workshop offers a comprehensive experience of traditional pottery techniques, guided by skilled local artisans.', 'full_description': 'Immerse yourself in the rich ceramic tradition of Morocco. You\'ll learn how to shape, decorate, and glaze pottery using techniques that have been passed down through generations. The workshop includes a tour of a local pottery studio, hands-on instruction, and the opportunity to create your own unique piece.', 'price': 45, 'duration': '3 hours', 'group_size': '6-10 participants', 'difficulty': 'Beginner', 'includes': [ 'All materials and tools', 'Professional instruction', 'Your own ceramic piece to take home', 'Traditional Moroccan mint tea' ], 'what_to_bring': [ 'Comfortable clothing', 'Enthusiasm to learn', 'Camera to capture your experience' ], 'image': '/static/images/workshops/ceramic-pottery-fez.jpg', 'hero_image': '/static/images/workshops/ceramic-pottery-in-fez.jpg', 'location': 'Traditional Artisan Quarter, Fez' }, 'leather-crafting': { 'name': 'Leather Crafting Class', 'slug': 'leather-crafting', 'description': 'Master the traditional Moroccan leather crafting techniques in a historic tannery.', 'full_description': 'Delve into centuries-old leather treatments, cutting, and stitching methods. This workshop includes a tannery visit and hands-on crafting of a small leather item.', 'price': 65, 'duration': '4 hours', 'group_size': '4-8 participants', 'difficulty': 'Intermediate', 'includes': [ 'All leather materials', 'Expert instruction', 'Guided tannery tour', 'Refreshments' ], 'what_to_bring': [ 'Closed-toe shoes', 'Comfortable clothes', 'A sense of adventure' ], 'image': '/static/images/workshops/leather-crafting-fez.jpg', 'hero_image': '/static/images/workshops/leather-crafting-in-fez.jpg', 'location': 'Historic Tannery District, Fez' }, 'moroccan-cooking': { 'name': 'Moroccan Cooking Class', 'slug': 'moroccan-cooking', 'description': 'Discover the secrets of Moroccan cuisine through a hands-on cooking experience.', 'full_description': 'Join us for a culinary adventure where you will learn to prepare traditional Moroccan dishes. This workshop includes a market tour, cooking class, and a delicious meal.', 'price': 55, 'duration': '5 hours', 'group_size': '4-10 participants', 'difficulty': 'All Levels', 'includes': [ 'All ingredients and cooking tools', 'Professional instruction', 'Market tour', 'Meal you prepared' ], 'what_to_bring': [ 'Comfortable clothing', 'Appetite for learning', 'Camera to capture your experience' ], 'image': '/static/images/workshops/moroccan-cooking-class.jpg', 'hero_image': '/static/images/workshops/moroccan-cooking-class-in-fez.jpg', 'location': 'Traditional Kitchen, Marrakech' }, 'calligraphy': { 'name': 'Calligraphy Workshop', 'slug': 'calligraphy', 'description': 'Learn the elegant art of Arabic calligraphy from a master calligrapher.', 'full_description': 'This workshop offers an introduction to the beautiful art of Arabic calligraphy. You will learn about different script styles, practice with traditional tools, and create your own calligraphic artwork.', 'price': 35, 'duration': '2 hours', 'group_size': '4-8 participants', 'difficulty': 'Beginner', 'includes': [ 'All calligraphy materials', 'Expert instruction', 'Your own calligraphic artwork', 'Refreshments' ], 'what_to_bring': [ 'Comfortable clothing', 'Enthusiasm to learn', 'Camera to capture your experience' ], 'image': '/static/images/workshops/calligraphy-fez.jpg', 'hero_image': '/static/images/workshops/calligraphy-workshop-in-fez.jpg', 'location': 'Art Studio, Fez' }, 'spice-blending': { 'name': 'Spice Blending Workshop', 'slug': 'spice-blending', 'description': 'Explore the world of Moroccan spices and learn to create your own unique blends.', 'full_description': 'Join us for a sensory journey through the vibrant world of Moroccan spices. You will learn to identify and blend spices, create your own unique spice mix, and understand the role of spices in Moroccan cuisine.', 'price': 40, 'duration': '2.5 hours', 'group_size': '4-8 participants', 'difficulty': 'All Levels', 'includes': [ 'All spices and blending tools', 'Professional instruction', 'Your own spice blend to take home', 'Refreshments' ], 'what_to_bring': [ 'Comfortable clothing', 'Curiosity for learning', 'Camera to capture your experience' ], 'image': '/static/images/workshops/spice-blending-fez.jpg', 'hero_image': '/static/images/workshops/spice-blending-workshop-in-fez.jpg', 'location': 'Spice Market, Marrakech' }, 'textile-weaving': { 'name': 'Textile Weaving Class', 'slug': 'textile-weaving', 'description': 'Learn traditional Berber textile weaving techniques passed down through generations.', 'full_description': 'Immerse yourself in the art of Berber textile weaving. This workshop includes hands-on instruction with traditional looms, creating your own textile piece, and learning about the cultural significance of Berber textiles.', 'price': 50, 'duration': '4 hours', 'group_size': '4-8 participants', 'difficulty': 'Intermediate', 'includes': [ 'All weaving materials', 'Expert instruction', 'Your own textile piece', 'Refreshments' ], 'what_to_bring': [ 'Comfortable clothing', 'Patience and creativity', 'Camera to capture your experience' ], 'image': '/static/images/workshops/textile-weaving-fez.jpg', 'hero_image': '/static/images/workshops/textile-weaving-workshop-in-fez.jpg', 'location': 'Weaving Cooperative, Atlas Mountains' } } workshop = workshops_data.get(slug) if not workshop: flash('Workshop not found', 'error') return redirect(url_for('workshops')) # Determine previous/next workshop workshop_keys = list(workshops_data.keys()) previous_workshop = None next_workshop = None if slug in workshop_keys: current_index = workshop_keys.index(slug) if current_index > 0: prev_slug = workshop_keys[current_index - 1] previous_workshop = { 'name': workshops_data[prev_slug]['name'], 'slug': prev_slug } if current_index < len(workshop_keys) - 1: nxt_slug = workshop_keys[current_index + 1] next_workshop = { 'name': workshops_data[nxt_slug]['name'], 'slug': nxt_slug } return render_template( 'workshop_detail.html', workshop=workshop, previous_workshop=previous_workshop, next_workshop=next_workshop ) def get_destination_by_slug(slug): destinations = DestinationData.get_destinations() # Assuming this returns a dict of destinations return destinations.get(slug) # Return the destination corresponding to the slug def get_related_tours(slug): tours = tours_data.get_all_tours() related_tours = [] for tour_slug, tour_data in tours.items(): if tour_data.get('destination', '').lower() == slug.lower(): related_tours.append({ 'name': tour_data.get('name', 'Unnamed Tour'), 'slug': tour_slug, 'duration': tour_data.get('duration', 0), 'price': tour_data.get('price', 0) }) return related_tours from datetime import datetime @app.route('/destination/') def destination_detail(slug): destination = DestinationData.get_destination(slug) if destination: destinations = list(DestinationData.get_destinations().values()) current_index = DestinationData.get_destination_index(slug) if current_index is not None: previous_index = (current_index - 1) if current_index > 0 else (len(destinations) - 1) next_index = (current_index + 1) if current_index < (len(destinations) - 1) else 0 previous_destination = destinations[previous_index] next_destination = destinations[next_index] return render_template('destination_detail.html', destination=destination, destinations=destinations, previous_destination=previous_destination, next_destination=next_destination, datetime=datetime) @app.route('/sitemap.xml') def sitemap(): return send_from_directory(app.static_folder, 'sitemap.xml') @app.route('/static/') def serve_static(filename): return send_from_directory(app.static_folder, filename) @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @app.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 @app.route('/send_email', methods=['POST']) def send_email(): name = request.form.get('name') email = request.form.get('email') phone = request.form.get('phone') subject = request.form.get('subject') message = request.form.get('message') msg = Message(subject, sender=app.config['MAIL_USERNAME'], recipients=[os.getenv('ADMIN_EMAIL')]) msg.body = f"Name: {name}\nEmail: {email}\nPhone: {phone}\n\nMessage:\n{message}" try: mail.send(msg) logger.info(f"Email sent to {os.getenv('ADMIN_EMAIL')}") return jsonify({'success': True}) except Exception as e: logger.error(f"Failed to send email: {e}") return jsonify({'success': False}) def format_admin_email(payment_details, tour): custom_info = payment_details.get('customBookingInfo', {}) paypal_details = payment_details.get('paypal_details', {}) return f""" New Booking Details: Tour: {tour.name} Name: {custom_info.get('name')} Email: {custom_info.get('email')} Phone: {custom_info.get('phone')} Travelers: {custom_info.get('travelers')} Travel Dates: {custom_info.get('travelDates')} Message: {custom_info.get('message')} Payment Details: Transaction ID: {paypal_details.get('id')} Amount: {paypal_details.get('purchase_units', [{}])[0].get('amount', {}).get('value')} {paypal_details.get('purchase_units', [{}])[0].get('amount', {}).get('currency_code')} Status: {paypal_details.get('status')} Payment made on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} """ @app.route('/payment/success', methods=['GET', 'POST']) def payment_success(): if request.method == 'POST': payment_details = request.json logger.info("Payment Details Received: %s", payment_details) # Extract custom booking information custom_info = payment_details.get('customBookingInfo', {}) tour_slug = payment_details.get('purchase_units', [{}])[0].get('description', '').split('-')[-1].strip() tour = Tour.query.filter_by(slug=tour_slug).first() if not tour: flash('Tour not found for the booking.', 'error') return {'status': 'error', 'message': 'Tour not found.'} # Send booking details via email to admin admin_email = os.getenv('ADMIN_EMAIL', os.getenv('EMAIL')) msg = Message( subject=f"New Booking: {tour.name}", sender=os.getenv('EMAIL'), recipients=[admin_email] ) msg.body = format_admin_email(payment_details, tour) try: mail.send(msg) logger.info(f"Booking email sent to {admin_email}") flash('Booking confirmed and admin notified!', 'success') return {'status': 'success', 'message': 'Payment processed and booking confirmed.'} except Exception as e: logger.error(f"Failed to send admin email: {e}") flash(f'Error sending admin email: {str(e)}', 'error') return {'status': 'error', 'message': 'Failed to send admin email.'} return render_template('payment_success.html') @app.route('/payment/error') def payment_error(): return render_template('payment_error.html') @app.route('/payment/cancelled') def payment_cancelled(): return render_template('payment_cancelled.html') @app.route('/section_perdue') def section_perdue(): # Ajoutez ici le contenu de la section perdue return render_template('section_perdue.html') if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=False) # Assurez-vous que debug est False en production