import json import redis from flask import request, make_response, abort, jsonify from pytz import timezone from src.config import TEST_SETMORE_APPOINTMENT_KEY from src.exceptions import EntryNotFoundError from src.models.MYOB import Invoice from src.controllers.controller_abstract import ControllerAbstract from src.models.Setmore import Appointment, API_INPUT_DATE_FORMAT, Setmore from datetime import datetime, timedelta from src.models.studio_booking import StudioBooking, studios from src.utils import flask_cache from src.utils.loggers import exceptions_log, setmore_log from src.views import studio_bookings_view class StudioBookingController(ControllerAbstract): @staticmethod def cancel_studio_invoice(appointment_key): """Modifies the appointment invoice with the provided appointment_key with the appropriate cancellation details. The modifications applied to depend on the appointment's date in accordance with Wwave's Cancellation Policy which states that appointments cancelled within 48 hours of the appointment start_time must be paid for by the customer in full. Appointments cancelled outside these circumstances are free of charge. Args: appointment_key(str): The unique appointment appointment_key of the appointment being deleted.""" if appointment_key is not None: invoice = Invoice.get_studio_invoice(appointment_key) current_time = datetime.now(timezone("Australia/Melbourne")) # Format the current date and time as dd-mm hh-mm cancellation_timestamp = current_time.strftime("%d-%m %H-%M") if invoice is not None: # Case 1: The appointment.start_time is in less than 48 hours (meaning cancellation is chargeable) if current_time + timedelta(days=2) >= invoice.datetime: # Updating the PO number and the journal memo with late cancellation details and the timestamp invoice.CustomerPurchaseOrderNumber = f"FC-48h {cancellation_timestamp} " + invoice.CustomerPurchaseOrderNumber invoice.JournalMemo = f"Cancelled - Full Charge {cancellation_timestamp} " + invoice.JournalMemo # Editing the lines on the invoice to transfer the charges to the 'Cancelled Studio' account. cancellation_line = invoice.Lines[0] cancellation_line.Account = studios['cancel'].account.shrink_to_dict() # Set the cancellation line total to the BalanceDueAmount cancellation_line.Total = invoice.BalanceDueAmount invoice.Lines = [cancellation_line] # Replaces all other existing lines on the invoice # Case 2: The appointment.start_time is more than 48 hours away, meaning the cancellation is free of charge else: # Updating the PO number and the journal memo with cancellation details and the timestamp invoice.CustomerPurchaseOrderNumber = f"NC+48h {cancellation_timestamp} " + invoice.CustomerPurchaseOrderNumber invoice.JournalMemo = f"Cancelled - No Charge {cancellation_timestamp} " + invoice.JournalMemo # Zero out the balance and invoice lines for a free cancellation invoice.BalanceDueAmount = 0.0 for index, line in enumerate(invoice.Lines): line.Total = 0.0 invoice.Lines[index] = line # Update the invoice in MYOB via PUT request print(f"Cancelling invoice {invoice.Number}") invoice.update() _return = make_response(json.loads(invoice.pickle(False)), 200) else: _return = make_response(f'An invoice with the appointment key {appointment_key} does not exist.') else: _return = make_response('Please provide an appointment key.', 400) return _return @classmethod def update_booking(cls, appointment_key: str): try: # Get raw JSON body as a plain string for exact Redis comparison raw_payload = request.get_data(as_text=True) request_data = json.loads(raw_payload) setmore_log.info(f"Processing update for appointment {appointment_key} with data: {request_data}") # Initialize Redis r = redis.Redis(host="WwaveAPI-redis", port=6379, db=0) redis_key = f"booking_update:{appointment_key}" # Compare raw string payloads try: cached = r.get(redis_key) if cached and cached.decode("utf-8") == raw_payload: setmore_log.info(f"Duplicate update blocked by Redis for {appointment_key}") return jsonify({"message": "Update already reported"}), 208 except Exception as e: setmore_log.warning(f"Redis cache parsing failed for {appointment_key}: {str(e)}") # Parse date if not request_data.get("start"): appointment_date = Setmore.setmore_datetime(request_data.get("start_zapier_time")) else: appointment_date = Setmore.setmore_datetime(request_data.get("start")) appointment_date = appointment_date.strftime(API_INPUT_DATE_FORMAT) setmore_log.info(f"Calculated appointment date: {appointment_date}") # Fetch and process appointment appointment = Appointment.get_appointment(appointment_key, appointment_date) if appointment: setmore_log.info(f"Found appointment: {appointment}") if flask_cache.get(appointment_key) == appointment.pickle(False): setmore_log.info(f"Update already reported in flask_cache for {appointment_key}") return jsonify({"message": "Update already reported"}), 208 if appointment_key == TEST_SETMORE_APPOINTMENT_KEY: setmore_log.info("Test key used.") return jsonify({"message": "Endpoint Test Successful"}), 202 StudioBooking(appointment=appointment).process_appointment_update() flask_cache.set(appointment_key, appointment.pickle(False), timeout=60) # ✅ Store full raw payload as-is r.set(redis_key, raw_payload, ex=3600) setmore_log.info(f"Update successful for {appointment_key}") return jsonify({"message": "Update Successful"}), 200 else: setmore_log.warning(f"Appointment no longer exists for {appointment_key}") return jsonify({"message": "Appointment no longer exists"}), 202 except EntryNotFoundError as e: setmore_log.error(f"Appointment invoice not found for {appointment_key}: {str(e)}") exceptions_log.exception(e) abort(404, description="Appointment invoice not found.") except Exception as e: setmore_log.exception(f"Error updating appointment for {appointment_key}: {str(e)}") return jsonify({"error": "Internal Server Error"}), 500 @classmethod def create_booking(cls): """Creates a studio booking in the system, invoicing it in MYOB and recording it in the integration database.""" data = json.loads(request.get_data().decode("utf-8")) appointment_key = data.get("appointment_key") # Test recognition if appointment_key == TEST_SETMORE_APPOINTMENT_KEY: return "Already reported.", 208 appointment_date = data.get("appointment_date") # Check that invoice doesn't already exist from src.models.MYOB import Invoice existing_invoice = Invoice.get_studio_invoice(appointment_key) if not existing_invoice: if not appointment_key or not appointment_date: abort(400) appointment_date = Setmore.setmore_datetime(appointment_date).strftime(API_INPUT_DATE_FORMAT) appointment = Appointment.get_appointment(appointment_key, appointment_date) booking = StudioBooking(appointment=appointment) booking.record() appointment.label = f"{appointment.datetime.strftime('%d/%m')} {booking.invoice.Number}" appointment.update() return "Successfully created booking", 201 else: return f"An invoice for appointment {appointment_key} already exists.", 208 @classmethod def studio_bookings_view(cls): """Returns the studio bookings view for the date provided in the request's JSON body.""" data = cls.unpack_data() date = data.get('date') from datetime import datetime date = datetime.strptime(date, "%Y-%m-%d") return studio_bookings_view(date), 200