
    j9                     Z   d Z ddlmZmZ ddlmZmZ ddlmZm	Z	m
Z
mZ ddlmZ ddlmZ  G d d      Zd	ed
efdZd	ededed
e	e   fdZ	 dde
e   dededed
e	e   f
dZ	 dde
e   dededed
ef
dZde
e   ded
eeef   fdZdededed
efdZdeded
efdZde	e   deded
efdZy) a  
Dashboard metrics service for Aimantis PMS.

Provides centralized, reusable calculation logic for:
- Average Daily Rate (ADR)
- Revenue per Available Room (RevPAR)
- Other derived hospitality metrics

Uses proper PMS stay-night expansion logic consistent with
ISTAT, city tax, and guest night services.

ORM Optimization Strategy:
- NO select_related() is used because we only access property_id (FK integer),
  not the full property object. Traversing FK objects would be wasteful.
- .only() explicitly selects the minimal fields required for calculations.
- This prevents N+1 queries while avoiding Django FieldError from deferred fields.
- For multi-tenant scale, structure_id filtering is applied at the DB level.
    )date	timedelta)DecimalROUND_HALF_UP)DictListOptionalTuple)Q)Bookingc                   0    e Zd ZdZg dZdedededefdZy)	StayNightRevenuez
    Represents revenue allocated to a single occupied room night.
    
    Immutable dataclass-like structure for tracking per-night revenue.
    
booking_idproperty_id
night_daterevenuer   r   r   r   c                 <    || _         || _        || _        || _        y Nr   )selfr   r   r   r   s        8/backend/dashboard/services/dashboard_metrics_service.py__init__zStayNightRevenue.__init__$   s    $&$    N)	__name__
__module____qualname____doc__	__slots__intr   r   r    r   r   r   r      s1    
 GI3 S d U\ r   r   bookingreturnc                     | j                   r| j                   dk  rt        d      S | j                  xs t        d      }t        t        | j                               }|dk(  rt        d      S ||z  S )a  
    Calculate per-night revenue for a booking using BASE PRICE only.
    
    ADR Formula: base_price / length_of_stay
    
    IMPORTANT:
    - Uses base_price (room revenue ONLY)
    - Excludes: cleaning_fee, city_tax, other_extra_fees, subtotal, total_price
    - This is the hospitality industry standard for ADR calculation
    
    Args:
        booking: Booking instance with base_price and length_of_stay
        
    Returns:
        Decimal per-night revenue, or Decimal('0') if invalid
    r   0)length_of_stayr   
base_pricestr)r!   r&   r%   s      r   calculate_per_night_revenuer(   +   sl    " !!W%;%;q%@s| ##3ws|JS!7!789Ns|&&r   window_start
window_endc                    | j                   r| j                  sg S | j                  sg S t        | j                   |      }t	        | j                  |      }||k\  rg S t        |       }|dk  rg S g }|}||k  rG|j                  t        | j                  | j                  ||             |t        d      z  }||k  rG|S )a%  
    Expand a single booking into individual revenue nights within a window.
    
    This is the core PMS logic that properly handles:
    - check_in_date is INCLUDED
    - check_out_date is EXCLUDED
    - Overlap calculation with the analysis window
    - Per-night revenue allocation
    
    Args:
        booking: Booking object
        window_start: Start of analysis window (inclusive)
        window_end: End of analysis window (exclusive)
        
    Returns:
        List of StayNightRevenue objects for each occupied night in window
    r   r      days)
check_in_datecheck_out_dater   maxminr(   appendr   idr   )r!   r)   r*   effective_starteffective_endper_night_revenuerevenue_nightscurrent_dates           r    expand_booking_to_revenue_nightsr:   I   s    .   (>(>		 '//>O..
;M -'	 4G<A	 N"L

&"::#//')		
 		q)) 
& r   structure_idonly_checked_inc                     t        ||      }| r|t        |       z  }|r|t        d      z  }t        t        j                  j	                  |      j                  ddddd	d
dd            S )af  
    Fetch bookings overlapping a date window with proper filtering.
    
    Uses PMS overlap logic:
    - booking.check_in_date < window_end
    - booking.check_out_date > window_start
    
    Args:
        structure_id: Structure ID for multi-tenant filtering (None = all)
        window_start: Window start date (inclusive)
        window_end: Window end date (exclusive)
        only_checked_in: If True, only include is_checked_in=True bookings
                         (legacy parameter, default=False for active-stay logic)
        
    Returns:
        List of Booking objects overlapping the window
    )check_in_date__ltcheck_out_date__gt)r;   T)is_checked_inr4   r   r;   r/   r0   r%   r&   platform)r   listr   objectsfilteronly)r;   r)   r*   r<   filterss        r   get_bookings_for_windowrG      s    0 $'G 1,// 14(($ w'		

 r   c                 "   t        | |||      }|syg }|D ]!  }t        |||      }|j                  |       # |syt        d |D              }t	        |      }	|	dk(  ry||	z  }
|
j                  t        d      t              }t        |      S )a  
    Calculate Average Daily Rate (ADR) for a date window.
    
    ADR Formula (Hospitality Industry Standard):
        ADR = Total Room Revenue / Occupied Room Nights
    
    Where:
    - Total Room Revenue = SUM(base_price for all active bookings)
    - Occupied Room Nights = COUNT(stay nights within window)
    - Uses base_price ONLY (excludes cleaning fees, city tax, extra services)
    
    Business Rules:
    - Active stay: check_in_date <= target_date < check_out_date
    - Check-out day is NOT counted as occupied
    - Zero-division protection: returns 0.0 if no occupied nights
    - Result rounded to 2 decimals using ROUND_HALF_UP
    
    Args:
        structure_id: Structure ID for filtering (None = all structures)
        window_start: Window start date (inclusive)
        window_end: Window end date (exclusive)
        only_checked_in: If True, only count checked-in bookings (legacy, default=False)
        
    Returns:
        ADR as float rounded to 2 decimals, or 0.0 if no occupied nights
    r;   r)   r*   r<           r!   r)   r*   c              3   4   K   | ]  }|j                     y wr   r   .0nights     r   	<genexpr>z+calculate_adr_for_window.<locals>.<genexpr>       F3E%3E   r   0.01rounding)	rG   r:   extendsumlenquantizer   r   float)r;   r)   r*   r<   bookingsall_revenue_nightsr!   r8   total_revenueoccupied_nightsadradr_roundeds               r   calculate_adr_for_windowrb      s    B '!!'	H  249%!

 	!!.1   F3EFFM,-O ! /
)C,,wv,GKr   todayc                     ||t        d      z   f||t        d      z   f||t        d      z   fd}i }|j                         D ]  \  }\  }}t        | ||d      ||<    |S )ak  
    Calculate all ADR metrics (today, 7-day, 30-day) in one optimized call.
    
    This minimizes database queries by batching booking fetches.
    
    Args:
        structure_id: Structure ID for filtering (None = all structures)
        today: Current date for calculations
        
    Returns:
        Dict with keys: today, next_7_days, next_30_days
    r,   r-         )rc   next_7_daysnext_30_daysFrI   )r   itemsrb   )r;   rc   windowsresultsmetric_namer)   r*   s          r   calculate_all_adr_metricsrm     s    $ !223uya'889	r(: :;G G3:==?//lJ7%%!!	 
 4C Nr   r/   r0   target_datec                 "    | |cxk  xr |k  S c S )a  
    Determine if a booking is active on a specific date.
    
    Active Stay Logic (PMS Standard):
    - check_in_date <= target_date < check_out_date
    - Check-out day is EXCLUSIVE (guest leaves, room becomes available)
    
    This is a pure function with no side effects, ideal for unit testing.
    
    Args:
        check_in_date: Booking check-in date
        check_out_date: Booking check-out date
        target_date: Date to check activity
        
    Returns:
        True if booking is active on target_date, False otherwise
        
    Examples:
        >>> is_active_stay_on_date(date(2026, 5, 14), date(2026, 5, 19), date(2026, 5, 14))
        True
        >>> is_active_stay_on_date(date(2026, 5, 14), date(2026, 5, 19), date(2026, 5, 19))
        False  # Check-out day is NOT active
        >>> is_active_stay_on_date(date(2026, 5, 14), date(2026, 5, 19), date(2026, 5, 13))
        False  # Before check-in
    r    )r/   r0   rn   s      r   is_active_stay_on_daterp   A  s    < K8.8888r   r_   total_possible_nightsc                 6    |dk(  ry| |z  dz  }t        |d      S )a  
    Calculate occupancy percentage with defensive zero-division protection.
    
    Formula: (occupied_nights / total_possible_nights) * 100
    
    Args:
        occupied_nights: Number of occupied room nights
        total_possible_nights: Total available room nights
        
    Returns:
        Occupancy percentage (0-100), rounded to 2 decimals
        Returns 0.0 if total_possible_nights is 0
        
    Examples:
        >>> calculate_occupancy_percentage(20, 100)
        20.0
        >>> calculate_occupancy_percentage(0, 100)
        0.0
        >>> calculate_occupancy_percentage(50, 0)
        0.0  # Defensive zero-division
    r   rJ   d      )round)r_   rq   	occupancys      r   calculate_occupancy_percentagerw   b  s,    2 ! #88C?IAr   r\   c                    | syg }| D ]!  }t        |||      }|j                  |       # |syt        d |D              }t        |      }|dk(  ry||z  }|j	                  t        d      t              }	t        |	      S )a  
    Calculate ADR from a list of bookings within a date window.
    
    This is a pure calculation function that accepts pre-fetched bookings,
    making it ideal for unit testing without database dependencies.
    
    ADR Formula:
        ADR = SUM(base_price for occupied nights) / COUNT(occupied room nights)
    
    Args:
        bookings: List of Booking objects (must have base_price, length_of_stay,
                  check_in_date, check_out_date, property_id)
        window_start: Window start date (inclusive)
        window_end: Window end date (exclusive)
        
    Returns:
        ADR as float rounded to 2 decimals, or 0.0 if no occupied nights
        
    Examples:
        >>> bookings = [MockBooking(base_price=270, length_of_stay=5, ...)]
        >>> calculate_adr_from_bookings(bookings, date(2026, 5, 14), date(2026, 5, 15))
        54.0  # 270 / 5 = 54 per night
    rJ   rK   c              3   4   K   | ]  }|j                     y wr   rM   rN   s     r   rQ   z.calculate_adr_from_bookings.<locals>.<genexpr>  rR   rS   r   rT   rU   )r:   rW   rX   rY   rZ   r   r   r[   )
r\   r)   r*   r]   r!   r8   r^   r_   r`   ra   s
             r   calculate_adr_from_bookingsrz     s    8  249%!

 	!!.1   F3EFFM,-O!
/
)C,,wv,GKr   N)F)r   datetimer   r   decimalr   r   typingr   r   r	   r
   django.db.modelsr   bookings.modelsr   r   r(   r:   r   boolrG   r[   rb   r'   rm   rp   rw   rz   r    r   r   <module>r      s  & % * . .  # ' 'W '<::: : 

	:B "	B3-BB B 	B
 
']BR "	F3-FF F 	F
 FR%3-%% 
#u*%Z999 9 
	9B @87m88 8 	8r   