
    ?&jS                       d Z ddlmZ ddlZddlZddlmZmZmZm	Z	m
Z
 ddlmZ ddlmZ ddlmZ ddlmZmZmZmZmZ dd	lmZmZ dd
lmZ ddlmZ ddlmZ  ej@                  d      Z!ejD                  jF                  Z$ejD                  jJ                  Z&ejD                  jN                  Z(ddZ)ddZ*	 	 	 	 	 	 	 	 ddZ+ddZ,dd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ-dd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ. G d d      Z/y)u  
alloggiati/service.py
=====================
Core orchestration layer for Alloggiati Web sync.

Implements AlloggiatiService.sync() — the single entry point called by the
API view for POST /api/alloggiati/sync.

Pipeline (8 steps):
  1. Validate input (structure exists, credential exists, date range valid)
  2. Fetch bookings in range (confirmed / checked-in, deduplicated)
  3. Transform guests via existing transformer
  4. Authenticate with Alloggiati Web (hard stop on failure)
  5. Send valid guest payloads via AlloggiatiClient
  6. Parse and classify the server response
  7. Persist AlloggiatiSyncLog (always — even on error)
  8. Return structured response to the view

Security rules:
  - Credentials are decrypted in memory only, never logged or re-persisted
  - No guest PII (names, document numbers) appears in logs or responses
  - AlloggiatiSyncLog stores only aggregated counts

GDPR compliance:
  - Only metadata (counts, status, date range) is persisted
    )annotationsN)AnyDictListOptionalTuple)transaction)Q)timezone)AlloggiatiAuthErrorAlloggiatiClientAlloggiatiClientErrorAlloggiatiNetworkErrorAlloggiatiResponseError)AlloggiatiCredentialAlloggiatiSyncLog)transform_booking_guests)Booking)	Structure
alloggiatic                    	 t         j                  j                  | j                               S # t        t
        f$ r t	        | d      w xY w)z
    Parse a YYYY-MM-DD string into a datetime.date.

    Raises:
        ValueError: With a human-readable message on failure.
    z+ must be a valid date in YYYY-MM-DD format.)datetimedatefromisoformatstrip
ValueErrorAttributeError)value
field_names     /backend/alloggiati/service.py_parse_dater!   B   sL    U}}**5;;=99' UJ<'RSTTUs	   ,/ Ac                ^    | |kD  rt        d      || z
  j                  dkD  rt        d      y)z
    Ensure date_from <= date_to and the range is not unreasonably large.

    Raises:
        ValueError: On invalid range.
    z'date_from cannot be later than date_to.in  z'Sync date range cannot exceed 366 days.N)r   days)	date_fromdate_tos     r    _validate_date_ranger&   O   s<     7BCC)!!C'BCC (    c                    t        t        j                  j                  | d||g      j	                  d      j                  d      j                         j                  dd            S )ah  
    Fetch bookings for the structure that overlap the given date range.

    Overlap condition: check_in_date < date_to AND check_out_date > date_from

    Filters:
      - is_checked_in=True  (confirmed / checked-in guests only)
      - Deduplication by pk (distinct)

    Returns:
        List of Booking instances with guests and structure prefetched.
    T)	structureis_checked_incheck_in_date__ranger)   guestscheck_in_dateid)listr   objectsfilterselect_relatedprefetch_relateddistinctorder_by)r)   r$   r%   s      r    _fetch_bookingsr6   \   s`    " "+W!5 	 	

 
	$		(	#		/4	(
 
r'   c                4   | j                   t        j                  k(  r>| j                  }| j                  }|r|st        d      t        j                  ||      S | j                  }| j                  }|r|st        d      t        j                  ||      S )ug  
    Instantiate an AlloggiatiClient from a credential record.

    Credentials are decrypted in memory here and passed directly to the
    client constructor — they are never stored in variables that outlive
    this function's scope beyond the client object itself.

    Raises:
        AlloggiatiAuthError: If required credential fields are missing.
    zADigital certificate or private key is missing for this structure.)certificate_pemprivate_key_pemzBAlloggiati Web username or password is missing for this structure.)usernamepassword)moder   MODE_DIGITAL_CERTIFICATEcertificateprivate_keyr   r   from_certificater:   r;   
from_codes)
credentialcertkeyr:   r;   s        r    _build_clientrE   z   s     .GGG%%$$3%S   00 
 	
 ""H""H8!P
 	
 &&8LLr'   )validation_errorsc        	            t         j                  j                  | |||t        j                         |||||xs g 
      S )u   
    Create and save an AlloggiatiSyncLog record.

    Always called — even on error — to maintain a complete audit trail.
    No PII (document numbers, credentials) is stored.
    validation_errors contains structured per-guest field errors only.
    )
r)   r$   r%   
started_atcompleted_atstatusmessageguests_sentguests_rejectedrF   )r   r0   creater   now	r)   r$   r%   rH   rJ   rK   rL   rM   rF   s	            r    _persist_sync_logrQ      sK    & $$++\\^'+1r ,  r'   c                6    | ||||rt        |      nd|xs g dS )z)Build the standardised API response dict.NrJ   sentrejectedrK   sync_log_idrF   )strrS   s         r    _build_responserX      s.     +6s;'D.4" r'   c                  p    e Zd ZdZe	 	 	 	 	 	 	 	 dd       Zedd       Zed	d       Ze	 	 	 	 d
d       Zy)AlloggiatiServiceu   
    Core orchestration service for Alloggiati Web sync.

    All public methods are static — no instance state is required.
    c                	   t        j                         }	 t        j                  |       }	 t        |d      }t        |d      }t        ||       	 t        j                  |      }t        |||      }
|
s5t        ||||t        ddd      }	t        t        ddd|	j                        S t        j                  |
      \  }}t!        |      }|D cg c]G  }|j#                  d      |j#                  d	      |j#                  d
d      |j#                  dg       dI }}|s>|}| d}t        ||||t        |d||	      }	t        t        d|||	j                  |      S 	 t%        |      }	 |j)                  |      }|j#                  dd      }|j#                  dd      }||z   }|j#                  d      r|dk(  r|dk(  rt0        }d| d}nL|j#                  d      r|dkD  rt        }d| d| d| d| d	}n t        }|j#                  d       xs d!}d"| }t        |||||||||	      }	t        |||||	j                  |      S # t        $ r'}t        t        ddt        |      d      cY d}~S d}~ww xY w# t        $ r'}t        t        ddt        |      d      cY d}~S d}~ww xY w# t        $ rQ}t        ||||t        t        |      dd      }	t        t        ddt        |      |	j                        cY d}~S d}~ww xY wc c}w # t&        $ r: t        ||||t        dd||	      }	t        t        d|d|	j                  |      cY S w xY w# t&        $ r: t        ||||t        dd||	      }	t        t        d|d|	j                  |      cY S t*        $ rG}t        ||||t        d| d||	      }	t        t        d|d| |	j                  |      cY d}~S d}~wt,        $ rG}t        ||||t        d| d||	      }	t        t        d|d| |	j                  |      cY d}~S d}~wt.        $ rG}t        ||||t        d| d||	      }	t        t        d|d| |	j                  |      cY d}~S d}~ww xY w)#a  
        Execute a full Alloggiati Web sync for a structure and date range.

        Args:
            structure_id: PK of the Structure (int or str).
            date_from:    Start of sync window, "YYYY-MM-DD".
            date_to:      End of sync window, "YYYY-MM-DD".

        Returns:
            {
                "status":            "CONNECTED" | "PARTIAL" | "ERROR",
                "sent":              int,
                "rejected":          int,
                "message":           str,
                "sync_log_id":       str | None,
                "validation_errors": [
                    {
                        "guest_id":   int,
                        "booking_id": int,
                        "guest_name": str,
                        "errors":     [{"field": str, "message": str}, ...]
                    },
                    ...
                ]
            }
        r   N)rJ   rT   rU   rK   rV   r$   r%   )r)   r$   r%   rH   rJ   rK   rL   rM   z8No checked-in bookings found in the selected date range.guest_id
booking_id
guest_namezUnknown Guesterrors)r\   r]   r^   r_   zF guest(s) failed validation and cannot be submitted to Alloggiati Web.rP   rS   z6Authentication failed: invalid or missing credentials.z"Authentication failed during send.zNetwork error: zServer response error: zAlloggiati client error: acceptedrU   successzSync completed successfully. z% guest(s) accepted by Alloggiati Web.zPartial sync: z guest(s) accepted, z rejected (z failed validation, z rejected by server).raw_messagezUnknown server error.zSync failed. Server message: )r   rO   rZ   _get_structurer   rX   STATUS_ERRORrW   r!   r&   _get_credentialrQ   r.   r6   STATUS_PARTIAL_transform_alllengetrE   r   send_guestsr   r   r   STATUS_CONNECTED)structure_idr$   r%   rH   r)   excd_fromd_torB   logbookingsvalid_payloadsinvalid_recordstransformer_rejectedrecserialisable_errorsnrK   clientsend_resultserver_acceptedserver_rejectedtotal_rejectedfinal_statusraw_msgs                            r    synczAlloggiatiService.sync   s   @ \\^

		)88FI	 K8Fw	2D .	*::9EJ. #9fd;## %%R !	C #%RFF  +<*J*J8*T'"?3 '5
 ' "ggj1!ggl3!gglOD!ggh3	 ' 	 5
 $A# % %  $# %% 4"5
C #%-FF"5 	":.F2Q	 ,,^<Kj &//*a8%//*a8-???9%/Q*>CW[\C\+L/"##HJ  __Y'NQ,>)L  11E!" #())="##8:  (L!oom4O8OG5gY?G
  !'*1

  #1
 	
]  	"#C  	  	"#C  	  	## %#C !	C ##CFF 	l5
R # 	## %#P 4"5
C ##-PFF"5 	4 # 	## %#< 4"5
C ##-<FF"5  & 	## %#)#/ 4"5
C ##-)#/FF"5  ' 	## %#1#7 4"5
C ##-1#7FF"5  % 	## %#3C59 4"5
C ##-3C59FF"5 	s   I	 $I< J/ AL L ,M 		I9I4.I94I9<	J,J'!J,'J,/	L	8AL>L	L	A MMA RR!<O#R#R/<P1+R1R=<Q?9R?Rc                    	 t        |       }	 t        j                  j                  |      S # t        t        f$ r t        d| d      w xY w# t        j                  $ r t        d| d      w xY w)zr
        Fetch a Structure by PK.

        Raises:
            ValueError: If not found or ID is invalid.
        zInvalid structure_id: .)pkzStructure with id=z does not exist.)int	TypeErrorr   r   r0   ri   DoesNotExist)rl   r   s     r    rc   z AlloggiatiService._get_structure  s    	I\"B	H$$((B(//	 :& 	I5l5EQGHH	I
 %% 	H1"5EFGG	Hs   . A A#A3c                    	 t         j                  j                  |       S # t         j                  $ r t	        d| j
                   d      w xY w)z
        Fetch the AlloggiatiCredential for a structure.

        Raises:
            ValueError: If no credential is configured.
        r)   z8No Alloggiati Web credentials configured for structure 'z)'. Please add credentials before syncing.)r   r0   ri   r   r   namer   s    r    re   z!AlloggiatiService._get_credential/  s[    	'//33i3HH#00 	NN##LN 	s	   " -Ac                P   g }g }t               }| D ]  }t        |      }|j                  dg       D ]M  }|j                  dd      |j                  dd      f}||v r,|j                  |       |j	                  |       O |j                  |j                  dg               ||fS )a  
        Run transform_booking_guests on every booking and aggregate results.

        Deduplicates valid payloads by (document_number, arrival_date) to
        prevent double-submission on retry.

        Returns:
            (valid_payloads, invalid_records)
        validdocument_number arrival_dateinvalid)setr   ri   addappendextend)rq   	all_validall_invalidseenbookingresultpayload	dedup_keys           r    rg   z AlloggiatiService._transform_all?  s     +-	,.EG-g6F!::gr2 KK 126KK3	 $#  ) 3 vzz)R89   +%%r'   N)rl   r   r$   rW   r%   rW   returnDict[str, Any])rl   r   r   r   )r)   r   r   r   )rq   List[Booking]r   z1Tuple[List[Dict[str, Any]], List[Dict[str, Any]]])	__name__
__module____qualname____doc__staticmethodr   rc   re   rg    r'   r    rZ   rZ      s     {
{
{
 {
 
	{
 {
B
 H H"   &&	:& &r'   rZ   )r   rW   r   rW   r   datetime.date)r$   r   r%   r   r   None)r)   r   r$   r   r%   r   r   r   )rB   r   r   r   )r)   r   r$   r   r%   r   rH   zdatetime.datetimerJ   rW   rK   rW   rL   r   rM   r   rF   Optional[List[Dict[str, Any]]]r   r   )rJ   rW   rT   r   rU   r   rK   rW   rV   zOptional[str]rF   r   r   r   )0r   
__future__r   r   loggingtypingr   r   r   r   r   	django.dbr	   django.db.modelsr
   django.utilsr   alloggiati.clientr   r   r   r   r   alloggiati.modelsr   r   alloggiati.transformerr   bookings.modelsr   structures.modelsr   	getLoggerloggerStatus	CONNECTEDrk   PARTIALrf   ERRORrd   r!   r&   r6   rE   rQ   rX   rZ   r   r'   r    <module>r      s  6 #   3 3 !  !  F ; # '			<	( %++55 "))11 ''--
U
D  	<MV 9=  	
 "     6 P 9=  	
   6 0K& K&r'   