
    |Si                    .   d Z ddlmZ ddlmZ ddlmZ ddlmZmZ ddl	m
Z
mZmZmZmZ ddlmZmZmZmZmZmZmZ dd	lmZmZmZ dd
lmZ ddlmZ ddlm Z  e
rddl!m"Z" ddl#m$Z$  ed       G d d             Z%e G d d             Z& G d d      Z'y)z&Wallet service for point transactions.    )annotations)	dataclass)datetime)DecimalROUND_HALF_UP)TYPE_CHECKINGAnyDictListOptional   )HookExecutionErrorHookRejectionErrorInsufficientBalanceErrorInvalidParamsErrorInvalidPointTypeErrorUserNotFoundErrorWalletOperationError)HookManagerHookTypePostHookError)HookContext)get_global_registry)transfer_completed)Model)WalletRepositoryProtocolT)frozenc                      e Zd ZU dZded<   ded<   ded<   ded<   ded	<   d
ed<   d
ed<   ded<   ded<   ded<   ded<   y)TransactionRecordz'Represents a wallet transaction record.
int | Noneidstrwtypeintiiduidtyper   amountbalance
trans_typedescrcdateDict[str, Any]
extra_dataN)__name__
__module____qualname____doc____annotations__     N/home/cursorai/projects/django-wallet-utils/src/django_wallet_utils/service.pyr   r      s?    1NJ	H	H
IOJJr5   r   c                  j    e Zd ZU dZded<   dZded<   dZded<   dZded	<   dZd
ed<   dZ	ded<   ddZ
y)TransactionResulta  
    Result of a wallet transaction with hook execution status.

    Attributes:
        success: Whether the transaction was successful
        transaction_id: Transaction ID if successful
        error_code: Error code if transaction failed (PRE hook rejection or validation error)
        error_message: Human-readable error message if failed
        error_details: Additional error context if failed
        post_hook_errors: List of errors from POST hooks (transaction still successful)
    boolsuccessNr    transaction_id
str | None
error_codeerror_messagezDict[str, Any] | Noneerror_detailszList[PostHookError] | Nonepost_hook_errorsc                \   | j                   | j                  d}| j                  r+| j                  | j                  | j                  xs i d|d<   | j
                  rJ| j
                  D cg c]1  }|j                  |j                  |j                  |j                  d3 c}|d<   |S c c}w )z(Convert to dictionary for API responses.)r:   r;   codemessagedetailserrorhookrC   rD   rE   warnings)	r:   r;   r=   r>   r?   r@   	hook_namerD   rE   )selfresulterrs      r6   to_dictzTransactionResult.to_dictD   s     ||"11

 ??----3F7O     00" 1C  MMNN"{{"{{	 1"F: "s   +6B))returnr-   )r/   r0   r1   r2   r3   r;   r=   r>   r?   r@   rN   r4   r5   r6   r8   r8   /   sH    
 M!%NJ%!J
! $M:$+/M(/3707r5   r8   c                     e Zd ZdZh dZdddZ	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZ		 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dd	Z
	 	 	 	 	 	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dd
ZddZ	 	 	 	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZedd       Zy)WalletServicez
    Python port of the legacy WalletPoint PHP class.

    The service works with a WalletRepositoryProtocol that only needs to define point types.
    All balance operations and transaction recording are handled automatically.
    >
   r!   r%   r&   r'   r,   r+   r#   r(   r)   r*   c                d    || _         || _        |rt        t                     | _        yd| _        y)a  
        Initialize the wallet service.

        Args:
            repository: WalletRepositoryProtocol instance that defines point types and provides User model
            enable_hooks: Whether to enable hook execution (default: False for backward compatibility)
        N)
repositoryenable_hooksr   r   hook_manager)rK   rS   rT   s      r6   __init__zWalletService.__init__v   s.     %(BNK(;(=>TXr5   Nc                   |i }| j                  d|||||d|       | j                  j                         }|j                  |d      }	t	        dt        |	            }	|j                  t        d      |	z  t              }
| j                  rX| j                  rL| j                  j                  ||      }t        d|||
|||||	      }	 | j                  j                  |       	 | j                  j)                  |||
d
      }t/        d|||d|
|||| j1                         |j                  di             }	 | j                  j3                  |      }g }| j                  r>| j                  r2t        d|||
|||||d|i
      }| j                  j5                  |      }| j                  rt        d||r|      S d      S |S # t        $ r7}t        d|j                   |j"                  |j$                  	      cY d}~S d}~wt&        $ r  w xY w# t*        $ r}t-        d|       |d}~ww xY w# t*        $ r}t-        d|       |d}~ww xY w)a  
        Add points to a user wallet balance.

        Args:
            user_id: The user ID
            point_type: The type of wallet to add to (e.g., "credit_balance")
            amount: The amount to add (must be positive)
            remarks: Description/remarks for the transaction
            trans_type: Transaction type code (integer constant, optional)
            params: Extra parameters dict with optional 'data' key for additional fields
            iid: Initiator ID - ID of the user who performed this transaction (-100 for system)

        Returns:
            The transaction ID (legacy behavior) or TransactionResult (if hooks are enabled)

        Raises:
            InvalidPointTypeError: If point type doesn't exist
            InvalidParamsError: If params are invalid
            WalletOperationError: If the operation fails
            HookRejectionError: If a PRE hook rejects the transaction
        NaddF   r   0.1rounding)		operationuser_id
point_typer(   remarksr*   r%   paramscurrent_balancer:   r=   r>   r?   allow_negativezError adding user point: cdatar!   r#   r%   r&   r'   r(   r)   r*   r+   r,   r.   #Error creating transaction record: r;   
r]   r^   r_   r(   r`   r*   r%   ra   rb   metadataTr:   r;   r@   )_validate_add_deduct_pointrS   get_point_typesgetmaxr$   quantizer   r   rT   rU   get_user_balancer   execute_pre_hooksr   r8   r=   rD   rE   r   update_balance	Exceptionr   r   _get_current_datetimecreate_transaction_recordexecute_post_hooks)rK   r^   r_   r(   r`   r*   ra   r%   point_typesdecimal_placesamount_roundedrb   contextebalance_afterrecordr;   r@   s                     r6   	add_pointzWalletService.add_point   s   > >F''7JUF	

 oo557$Q7QN 34  >)ITab !2!2"oo>>w
SO!%%% /
G
!!33G<	O OO::7JP^ot:uM
 #!!!,,.zz&"-
	Y!__FFvNN
 !2!2!%%% -*N;G  $00CCGL $-5E!1  LP  y & (! ||"#))"#))	  &   	O&)B1#'FGQN	O&  	Y&)LQC'PQWXX	YsN   
G &H :H2 	H,H;HH	H/H**H/2	I;I

Ic	                   |i }| j                  d|||||||       | j                  j                         }	|	j                  |d      }
t	        dt        |
            }
|j                  t        d      |
z  t              }| j                  r[| j                  rO| j                  j                  ||      }t        d||||||||d|i
      }	 | j                  j                  |       	 | j                  j)                  ||||      \  }}|s~| j                  j                  ||      }t/        |      }t/        |      }| j                  r"t        d	dd|d|
 dd|d|
 d||d
      S t1        d|d|
 dd|d|
 d||      t3        d|||d||||| j5                         |j                  di             }	 | j                  j7                  |      }g }| j                  r?| j                  r3t        d||||||||||d
      }| j                  j9                  |      }| j                  rt        d||r|      S d      S |S # t        $ r7}t        d	|j                   |j"                  |j$                  
      cY d}~S d}~wt&        $ r  w xY w# t*        $ r}t-        d|       |d}~ww xY w# t*        $ r}t-        d|       |d}~ww xY w)a  
        Deduct points from a user wallet balance using atomic SQL operation.

        Uses SQL WHERE clause to prevent race conditions and ensure balance sufficiency.

        Args:
            user_id: The user ID
            point_type: The type of wallet to deduct from
            amount: The amount to deduct (must be positive)
            remarks: Description/remarks for the transaction
            trans_type: Transaction type code (integer constant, optional)
            allow_negative: Whether to allow negative balance (default: False)
            params: Extra parameters dict with optional 'data' key for additional fields
            iid: Initiator ID - ID of the user who performed this transaction (-100 for system)

        Returns:
            The transaction ID (legacy behavior) or TransactionResult (if hooks are enabled)

        Raises:
            InsufficientBalanceError: If insufficient balance and allow_negative=False
            InvalidPointTypeError: If point type doesn't exist
            InvalidParamsError: If params are invalid
            WalletOperationError: If the operation fails
            HookRejectionError: If a PRE hook rejects the transaction
        NdeductrY   r   rZ   r[   re   rj   Frc   rd   zError deducting user point: INSUFFICIENT_BALANCEz6Insufficient balance for this transaction. Available: .fz, Requested: )	available	requesteddrg   rh   ri   )r;   re   Trl   )rm   rS   rn   ro   rp   r$   rq   r   r   rT   rU   rr   r   rs   r   r8   r=   rD   rE   r   deduct_balance_atomicru   r   floatr   r   rv   rw   rx   )rK   r^   r_   r(   r`   r*   re   ra   r%   ry   rz   r{   rb   r|   r}   r:   r~   r   r   r   r;   r@   s                         r6   deduct_pointzWalletService.deduct_point   s   H >F''gz67JX^	

 oo557$Q7QN 34  >)ITab !2!2"oo>>w
SO!"%%% /*N;G
!!33G<	R%)__%J%J^N &K &"G] "oo>>w
SOo.IfI  (!5$Z[defgufvvwdwZx y//8>:J!8K.L#N &/%.#	 	 /LYWXYgXhhiViLj k""+An-=Q+>!?A''	  #!!!,,.zz&"-
	Y!__FFvNN
 !2!2!"%%% -,:n]G  $00CCGL $-5E!1  LP  s & (! ||"#))"#))	  &   	R&)EaS'IJPQQ	RZ  	Y&)LQC'PQWXX	YsN   I )"J  J9 	J,JJJ	J6"J11J69	KKKc
                X   ddl m}
 ddlm} ||}|sd}|dk  rt	        dd	      | j                  ||       | j                  ||       	 | j                  j                  |d
       | j                  r`| j                  rT| j                  j                  ||      }t!        d||||||	||d|xs i i|      }	 | j                  j#                  |       |xs i }| j                  }d
| _        	 |
j/                         5  di |d|ii}| j1                  |||| d| d|d
||	      }di |d|ii}| j3                  |||| d| d|||	      }| j                  j                  ||      }| j                  j                  ||      }| j5                  |      }| j5                  |      }ddd       || _         d }d||||||||d}|r| j                  r|t!        d||||||	||d|i||||||d       }| j                  j7                  |      }|r@|D cg c]1  }|j8                  |j&                  |j(                  |j*                  d!3 c}|d"<   t;        j<                  | j>                  |||||||#
       |S # t        $ r8}dt        t        |      j                        v rt        d| d|      | d}~ww xY w# t$        $ r1}d
|j&                  |j(                  |j*                  ddcY d}~S d}~wt,        $ r  w xY w# 1 sw Y   exY w# || _        w xY wc c}w )$u  
        Transfer points between two users atomically.

        The transfer is wrapped in a database transaction to ensure both deduction
        and addition succeed or fail together. Supports same-type transfers
        (e.g., cash → cash) and cross-type transfers (e.g., cash → credit).

        Args:
            from_user_id: The sender's user ID
            to_user_id: The recipient's user ID
            from_point_type: The point type to deduct from sender
            to_point_type: The point type to add to recipient
            amount: The amount to transfer (must be positive)
            remarks: Description for the transaction (default: "Point transfer")
            trans_type: Transaction type code (defaults to WALLET_TRANSFER)
            metadata: Optional metadata dict to attach to both transaction records
            iid: Initiator ID - ID of the user who performed this transaction (-100 for system)

        Returns:
            Dict with keys:
                - success: True if successful, False if rejected by hook
                - transfer_id: Unique transfer identifier (from_transaction_id:to_transaction_id)
                - from_transaction_id: Transaction ID for sender's deduction
                - to_transaction_id: Transaction ID for recipient's addition
                - from_user_id: Sender's user ID
                - to_user_id: Recipient's user ID
                - from_point_type: Sender's point type
                - to_point_type: Recipient's point type
                - amount: Transfer amount
                - from_balance: Sender's balance after transfer
                - to_balance: Recipient's balance after transfer
                - error: Error details if rejected (only if hooks enabled)
                - warnings: POST hook errors if any (only if hooks enabled)

        Raises:
            UserNotFoundError: If recipient doesn't exist
            InsufficientBalanceError: If sender has insufficient balance (only if hooks disabled)
            InvalidPointTypeError: If point type doesn't exist
            InvalidParamsError: If parameters are invalid
            WalletOperationError: If the transfer fails
            HookRejectionError: If a PRE hook rejects the transaction (only if hooks disabled)

        Example:
            # Same point type transfer
            result = service.transfer_points(
                from_user_id=1,
                to_user_id=2,
                from_point_type="cash_balance",
                to_point_type="cash_balance",
                amount=Decimal("100.00"),
                remarks="Transfer to friend",
            )

            # Cross-type transfer
            result = service.transfer_points(
                from_user_id=1,
                to_user_id=2,
                from_point_type="cash_balance",
                to_point_type="credit_balance",
                amount=Decimal("50.00"),
                remarks="Cash to credit conversion",
            )
        r   )transactionr   )WALLET_TRANSFERNzPoint transferz Transfer amount must be positivetransfer_pointsmethodF)
for_updateDoesNotExistzRecipient user with ID z does not exist)r^   transferrg   )r]   r^   r_   r(   r`   r*   r%   
to_user_idto_point_typera   rb   rB   )r:   rF   transfer_toz
 (to user ))r^   r_   r(   r`   r*   re   ra   r%   transfer_fromz (from user )r^   r_   r(   r`   r*   ra   r%   :T)r:   transfer_idfrom_transaction_idto_transaction_idfrom_user_idr   from_point_typer   r(   from_balance
to_balance)r   r   r   r   r   )r]   r^   r_   r(   r`   r*   r%   r   r   ra   rb   rk   rG   rI   )
senderr   r   r   r   r   r(   from_record	to_recordrk   ) 	django.dbr   transaction_typesr   r   _validate_user_point_typerS   _get_balance_objectru   r"   r'   r/   r   rT   rU   rr   r   rs   r   r=   rD   rE   r   atomicr   r   get_transaction_by_idrx   rJ   r   send	__class__)rK   r   r   r   r   r(   r`   r*   rk   r%   db_transactionr   r}   rb   r|   transfer_metadataoriginal_enable_hooksfrom_paramsr   	to_paramsr   r   r   r   r   r   rL   r@   rM   s                                r6   r   zWalletService.transfer_points  s   V 	<6 (J &G Q;$%GPabb 	&&|_E&&z=A	OO//
u/M !2!2"oo>>|_]O!$$*%%+B/ /G!!33G< %N !% 1 1!%	6&&(%'W*;'W]J'WX&*&7&7(.!&iz*Q?)#(& '8 	'# $%Y(9%Y?L%YZ	$(NN&,!&i|L>C)$ %3 %!  $??o^!__==j-X
 #889LM 667HI	A )F !6D --Q/@.AB &#6!2($.*($
 !T%6%6!$$*%%+ 12 ,#.+>):$0",G(  $00CCGL  0&  0 !$ ##&;;#&;;	  0&z" 	>>#%!+'#&	
 q  	T!W%5%5!66'-j\I&  	6 & $ !#$99#$99  &  )(F !6DZ&sa   J :K /L ?B,L+L 6L'	K3K  K	L&K=7L=LLL 	L$c                b    | j                   j                         }||vrt        d| d|      y)z
        Validate whether the given point type is valid.

        Raises:
            InvalidPointTypeError: If point type doesn't exist
        No such point (	) exists!r_   N)rS   rn   r   )rK   r^   r_   valid_typess       r6   r   z'WalletService._validate_user_point_type  s>     oo557[('!*Y7J  )r5   c	                   | j                  ||       |dk(  rdnd}	t        |t              st        d|	 d|	      d|v r|d   rt        |d   t              st        d|	 d|	      g }
|d   j	                         D ]"  }|| j
                  v s|
j                  |       $ |
r%d	j                  |
      }t        d|	 d
| d|	      yyy)z
        Validate parameters for add/deduct point operations.

        Raises:
            InvalidPointTypeError: If point type doesn't exist
            InvalidParamsError: If params are invalid
        rX   r   r   zInvalid params for z(), it must be a dict!r   rg   zInvalid params["data"] for z, z-(), some of the data parameters are invalid: !N)r   
isinstancedictr   keysRESERVED_FIELDSappendjoin)rK   
add_deductr^   r_   r(   r`   r*   re   ra   r   invalid_fieldsfield
fields_strs                r6   rm   z(WalletService._validate_add_deduct_point  s    $ 	&&w
; *e 3&$'$':6(BX%YbhiiVvfVnd3(1&9OPY_   N,,.D000"))%0 / !YY~6
(1&9fgqfrrst!   !/r5   c
                   | j                   j                         }
|
j                  j                         }||j	                  |      }|A| j                   j                         }||vrt        d| d|      |j	                  |      }||j	                  |      }|%|dvrt        d	| d
      |j	                  |      }||j	                  |      }|7t        |t              rt        j                  |      }|j	                  |      }|7t        |t              rt        j                  |      }|j	                  |      }|j                  d      }|	dkD  r||	d }||d| }g }|D ]  }t        |j                  |j                  |j                   |j"                  |j$                  |j&                  |j(                  |j*                  |j,                  t/        |j0                  d      r|j0                  j3                         nt        |j0                        |j4                  xs i       }|j7                  |        |S )a  
        Retrieve wallet transaction history with various filters.
        
        Args:
            user_id: Filter by user ID (uid field)
            point_type: Filter by wallet/point type (wtype field)
            trans_type: Filter by transaction type code (trans_type field, integer constant)
            transaction_type: Filter by transaction type ('c' for credit, 'd' for debit) (type field)
            iid: Filter by initiator ID (iid field) - user who performed the transaction (-100 for system)
            start_date: Filter transactions from this date (inclusive) (cdate field)
            end_date: Filter transactions until this date (inclusive) (cdate field)
            limit: Maximum number of records to return
            offset: Number of records to skip (for pagination)
        
        Returns:
            List of TransactionRecord objects
        
        Example:
            # Get all transactions for a user
            transactions = service.get_transaction_history(user_id=123)
            
            # Get credit transactions for a specific point type
            transactions = service.get_transaction_history(
                user_id=123,
                point_type="credit_balance",
                transaction_type="c",
            )
            
            # Get transactions by transaction type code
            from django_wallet_utils.transaction_types import WALLET_DEPOSIT
            transactions = service.get_transaction_history(
                user_id=123,
                trans_type=WALLET_DEPOSIT,
            )
            
            # Get transactions in date range
            from datetime import datetime, timedelta
            end = datetime.now()
            start = end - timedelta(days=30)
            transactions = service.get_transaction_history(
                user_id=123,
                start_date=start,
                end_date=end,
                limit=100,
            )
        Nr&   r   r   r   r#   r*   rf   r   Invalid transaction_type: &. Must be 'c' (credit) or 'd' (debit).r'   r%   
cdate__gte
cdate__ltez-cdater   	isoformatrh   )rS   get_wallet_modelobjectsallfilterrn   r   r   r   r"   r   fromisoformatorder_byr   r!   r#   r%   r&   r'   r(   r)   r*   r+   hasattrr,   r   r.   r   )rK   r^   r_   r*   transaction_typer%   
start_dateend_datelimitoffsetwallet_modelquerysetr   recordsobjr   s                   r6   get_transaction_historyz%WalletService.get_transaction_history  s8   t 779''++- 73H!//99;K,+%j\;
   Z8H!*=H'z1(01A0BBhi   ,<=H?3/H!*c*%33J?
*=H(C(#11(;(;H $$X. A:(H'H C&66iiGGGGXXzz>>ii/6syy+/Ncii))+TWX[XaXaTb>>/RF NN6"   r5   c                   | j                   j                         }	 |j                  j                  |      }t	        |j
                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  t        |j                  d      r|j                  j!                         nt#        |j                        |j$                  xs i       S # |j&                  $ r Y yw xY w)z
        Get a single transaction by its ID.
        
        Args:
            transaction_id: The transaction ID
        
        Returns:
            TransactionRecord if found, None otherwise
        )r!   r   rh   N)rS   r   r   ro   r   r!   r#   r%   r&   r'   r(   r)   r*   r+   r   r,   r   r"   r.   r   )rK   r;   r   r   s       r6   r   z#WalletService.get_transaction_by_idP  s     779	&&**n*=C$66iiGGGGXXzz>>ii/6syy+/Ncii))+TWX[XaXaTb>>/R  (( 		s   CC9 9D
Dc                   | j                   j                         }|j                  j                         }	||	j	                  |      }	|A| j                   j                         }
||
vrt        d| d|      |	j	                  |      }	||	j	                  |      }	|%|dvrt        d| d	      |	j	                  |
      }	||	j	                  |      }	|7t        |t              rt        j                  |      }|	j	                  |      }	|7t        |t              rt        j                  |      }|	j	                  |      }	|	j                         S )a  
        Count transactions matching the given filters.
        
        Args:
            user_id: Filter by user ID (uid field)
            point_type: Filter by wallet/point type (wtype field)
            trans_type: Filter by transaction type code (trans_type field, integer constant)
            transaction_type: Filter by transaction type ('c' for credit, 'd' for debit) (type field)
            iid: Filter by initiator ID (iid field) - user who performed the transaction (-100 for system)
            start_date: Filter transactions from this date (inclusive) (cdate field)
            end_date: Filter transactions until this date (inclusive) (cdate field)
        
        Returns:
            Number of transactions matching the filters
        r   r   r   r   r   r   r   r   r   r   r   r   r   )rS   r   r   r   r   rn   r   r   r   r"   r   r   count)rK   r^   r_   r*   r   r%   r   r   r   r   r   s              r6   count_transactionsz WalletService.count_transactionsm  sd   2 779''++- 73H!//99;K,+%j\;
   Z8H!*=H'z1(01A0BBhi   ,<=H?3/H!*c*%33J?
*=H(C(#11(;(;H~~r5   c                 F    t        j                         j                         S )z*Get current datetime as ISO format string.)r   nowr   r4   r5   r6   rv   z#WalletService._get_current_datetime  s     ||~''))r5   )F)rS   z'WalletRepositoryProtocol'rT   r9   )NN)r^   r$   r_   r"   r(   r   r`   r"   r*   r    ra   Optional[Dict[str, Any]]r%   r$   rO   int | TransactionResult)NFNr   )r^   r$   r_   r"   r(   r   r`   r"   r*   r    re   r9   ra   r   r%   r$   rO   r   ) NNr   )r   r$   r   r$   r   r"   r   r"   r(   r   r`   r"   r*   r    rk   r   r%   r$   rO   r-   )r^   r$   r_   r"   rO   None)r   r"   r^   r$   r_   r"   r(   r   r`   r"   r*   r    re   r9   ra   r-   rO   r   )	NNNNNNNNr   )r^   r    r_   r<   r*   r    r   r<   r%   r    r   datetime | str | Noner   r   r   r    r   r$   rO   zlist[TransactionRecord])r;   r$   rO   zTransactionRecord | None)NNNNNNN)r^   r    r_   r<   r*   r    r   r<   r%   r    r   r   r   r   rO   r$   )rO   r"   )r/   r0   r1   r2   r   rV   r   r   r   r   rm   r   r   r   staticmethodrv   r4   r5   r6   rQ   rQ   `   s   O
Y$ "&+/{{ { 	{
 { { ){ { 
!{F "&$+/^^ ^ 	^
 ^ ^ ^ )^ ^ 
!^N !%-1yy y 	y
 y y y y +y y 
yv)) ) 	)
 ) ) ) ) ) 
)Z #!%!%'+,0*. || | 	|
 %| | *| (| | | 
!||> #!%!%'+,0*.@ @  @  	@ 
 %@  @  *@  (@  
@ D * *r5   rQ   N)(r2   
__future__r   dataclassesr   r   decimalr   r   typingr   r	   r
   r   r   
exceptionsr   r   r   r   r   r   r   django_package_hooksr   r   r   hooksr   r   r   signalsr   django.db.modelsr   rS   r   r   r8   rQ   r4   r5   r6   <module>r      s    , " !  * ; ;   F E  ! '&4 $    - - -`R* R*r5   