
    o=i>                        d 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
 ddlmZ erddlmZ  G d d	e      Z G d
 d      Zy)z?Default Django repository implementation for wallet operations.    )annotations)Decimal)TYPE_CHECKINGProtocol)
connectiontransaction)F)Modelc                  d    e Zd ZdZddZ	 d		 	 	 	 	 	 	 	 	 d
dZ	 d		 	 	 	 	 	 	 	 	 ddZddZddZy)WalletRepositoryProtocola  
    Interface for wallet operations.
    
    For most use cases, use WalletRepository which implements all methods automatically.
    You only need to define point types and their decimal places.
    
    Custom implementations must implement all methods below.
    c                     y)z
        Return a dictionary mapping point types to their decimal places.
        
        Example:
            {
                "credit_balance": 2,
                "reward_points": 0,
                "crypto_balance": 8,
            }
        N selfs    Z/home/cursorai/projects/telegram-earn/packages/wallet_utils/src/wallet_utils/repository.pyget_point_typesz(WalletRepositoryProtocol.get_point_types   s     	    c                     y)z
        Update user balance by adding amount (positive for add, negative for deduct).
        
        Returns the new balance after the update.
        Should be atomic (use database transactions).
        Nr   r   user_id
point_typeamountallow_negatives        r   update_balancez'WalletRepositoryProtocol.update_balance&   s     	r   c                     y)a  
        Atomically deduct balance using SQL WHERE clause to prevent race conditions.
        
        Uses SQL: UPDATE ... WHERE {point_type} >= {amount} (or >= 0 if allow_negative)
        
        Returns:
            Tuple of (success: bool, new_balance: Decimal)
            - success: True if row was updated (sufficient balance), False otherwise
            - new_balance: The balance after deduction (if successful) or current balance (if failed)
        
        Should be atomic (use database transactions).
        Nr   r   s        r   deduct_balance_atomicz.WalletRepositoryProtocol.deduct_balance_atomic5   s    & 	r   c                     y)z
        Get the current balance for a specific point type for a user.
        
        Used for reading balance after operations.
        Nr   )r   r   r   s      r   get_user_balancez)WalletRepositoryProtocol.get_user_balanceJ   s     	r   c                     y)z
        Create a transaction record and return the new transaction ID.
        
        Args:
            record: TransactionRecord dataclass instance
        
        Should be atomic (use database transactions).
        Nr   )r   records     r   create_transaction_recordz2WalletRepositoryProtocol.create_transaction_recordR   s     	r   Nreturndict[str, int]F
r   intr   strr   r   r   boolr#   r   
r   r'   r   r(   r   r   r   r)   r#   ztuple[bool, Decimal]r   r'   r   r(   r#   r   r#   r'   )	__name__
__module____qualname____doc__r   r   r   r   r!   r   r   r   r   r      s    $  %  	
  
(  %  	
  
*	r   r   c                     e Zd ZdZ	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 ddZddZddZddZddZddZ	ddd	Z
ej                  	 d	 	 	 	 	 	 	 	 	 dd
       Zej                  	 d	 	 	 	 	 	 	 	 	 dd       ZddZej                  dd       Zy)WalletRepositorya  
    Default Django implementation of WalletRepositoryProtocol.
    
    This repository handles all balance operations and transaction recording automatically.
    Users only need to define point types and their decimal places - that's it!
    
    Example:
        # Using User model for balances (default)
        repo = WalletRepository(
            user_model=User,
            point_types={
                "credit_balance": 2,
                "reward_points": 0,
            },
        )
        
        # Using separate Wallet model for balances
        repo = WalletRepository(
            user_model=User,
            wallet_balance_model=Wallet,
            point_types={
                "credit_balance": 2,
                "reward_points": 0,
            },
        )
    Nc                    |ddl m} |}|| _        || _        || _        || _        |xs i | _        || _        |xs || _        |du| _	        y)a  
        Initialize the repository.
        
        Args:
            user_model: Django User model class (used for reference, or as balance model if wallet_balance_model not provided)
            point_types: Dictionary mapping point types to decimal places
            wallet_balance_model: Optional separate model for storing wallet balances (must have user_id field).
                                 If None, balances are stored on user_model.
            wallet_model: Optional custom WalletTransaction model (defaults to WalletTransaction)
            point_type_field_map: Optional mapping from point_type to database field name
            wallet_user_id_field: Field name in wallet_balance_model that references user_id (default: "user_id")
        N   )WalletTransaction)
modelsr5   _user_model_wallet_balance_model_point_types_wallet_model_point_type_field_map_wallet_user_id_field_balance_model_balance_model_is_separate)r   
user_modelpoint_typeswallet_balance_modelwallet_modelpoint_type_field_mapwallet_user_id_fieldr5   s           r   __init__zWalletRepository.__init__z   sc    , 1,L%%9"')%9%?R"%9" 3@j*>d*J'r   c                    | j                   S )z,Return point types and their decimal places.)r9   r   s    r   r   z WalletRepository.get_point_types   s       r   c                    | j                   S )zReturn the User model class.)r7   r   s    r   get_user_modelzWalletRepository.get_user_model   s    r   c                    | j                   S )z)Return the WalletTransaction model class.)r:   r   s    r   get_wallet_modelz!WalletRepository.get_wallet_model   s    !!!r   c                    | j                   S )z2Return the model used for storing wallet balances.)r=   r   s    r   get_wallet_balance_modelz)WalletRepository.get_wallet_balance_model   s    """r   c                :    | j                   j                  ||      S )z0Return the database field name for a point type.)r;   get)r   r   s     r   get_point_type_fieldz%WalletRepository.get_point_type_field   s    ))--j*EEr   c                "   d}| j                   r|rA	  | j                  j                  j                         j                  d
i | j
                  |iS ||k(  r; | j                  j                  j                  d
i | j
                  |idi i\  }}|S  | j                  j                  j                  d
i | j
                  |iS |r5	 | j                  j                  j                         j	                  |      S 	 | j                  j                  j	                  |      S # | j                  j                  $ r; ||k(  r4 | j                  j                  j                  d
i | j
                  |icY S  w xY w# | j                  j                  $ rp ||k(  rii }t        | j                  d      rd| |d<   t        | j                  d      r	d| d|d<    | j                  j                  j                  d
d	|i|cY S  w xY w# | j                  j                  $ rp ||k(  rii }t        | j                  d      rd| |d<   t        | j                  d      r	d| d|d<    | j                  j                  j                  d
d	|i|cY S  w xY w)a#  
        Get the balance object (wallet or user) for a given user_id.
        Creates the object if it doesn't exist ONLY for system users (user_id == -100).
        For regular users, raises DoesNotExist if user doesn't exist.
        
        Args:
            user_id: The user ID
            for_update: Whether to use select_for_update() for locking
            
        Returns:
            The balance model instance
            
        Raises:
            DoesNotExist: If user doesn't exist and user_id is not -100 (system user)
        idefaults)pkusernamesystem_user_emailsystem_z@system.localrR   r   )r>   r=   objectsselect_for_updaterN   r<   DoesNotExistcreateget_or_creater7   hasattr)r   r   
for_updateSYSTEM_USER_IDobjcreatedrQ   s          r   _get_balance_objectz$WalletRepository._get_balance_object   s     **
N4..66HHJNN 55w?  n,#L4#6#6#>#>#L#L $55w?$!#$LC J:4..66:: 55w? 
 ++33EEGKKwKWW++337777CCQ **77 .0At22::AA  #997C   4 ''44 
.0#%"4#3#3Z@5A'3KHZ0"4#3#3W=29'-0PHW->t//77>>V'VXVV
 ''44 	.0#%"4#3#3Z@5A'3KHZ0"4#3#3W=29'-0PHW->t//77>>V'VXVV	s8   ?D" 3E9 <%H "AE64E69BH HBJJc                   | j                  |      }|sA| j                  |d      }t        ||      }||z   dk  rt        d| dt	        |             | j
                  xs | j                  }| j                  r| j                  }	nd}	|	|i}
  |j                  j                  di |
j                  di |t        |      |z   i | j                  |d      }t        ||      }|S )z
        Update user balance by adding amount (positive for add, negative for deduct).
        
        Returns the new balance after the update.
        Fr]   r   zInsufficient balance: z < idr   )rO   ra   getattr
ValueErrorabsr8   r7   r>   r<   rW   filterupdater	   )r   r   r   r   r   
field_namebalance_objcurrent_balancebalance_modeluser_id_fieldfilter_kwargsnew_balances               r   r   zWalletRepository.update_balance  s    ..z:
 227u2MK%k:>O'!+ #9/9J#cRXk]![\\ 22Fd6F6F ** 66M !M '0 	=$$$5}5<< 	
1Z=612	

 ..w5.Ik:6r   c           
     ~   | j                  |      }| j                  j                  j                  }|dk(  r| j	                  ||      }d|fS | j
                  r8|r| j                   d}|g}	n| j                   d| d}||g}	| j                  }
n|rd}|g}	n
d| d}||g}	d}
t        j                         5 }d	| d
| d| d| d	}|j                  ||g|	z          |j                  }|dkD  rQ|j                  d| d| d|
 d|g       |j                         }t        t        |d               }d|fcddd       S |j                  d| d| d|
 d|g       |j                         }|rt        t        |d               }n| j                  |d       t        d      }d|fcddd       S # 1 sw Y   yxY w)a  
        Atomically deduct balance using SQL WHERE clause to prevent race conditions.
        
        Uses SQL: UPDATE ... WHERE {point_type} >= {amount} (or >= 0 if allow_negative)
        
        Returns:
            Tuple of (success: bool, new_balance: Decimal)
            - success: True if row was updated (sufficient balance), False otherwise
            - new_balance: The balance after deduction (if successful) or current balance (if failed)
        r   Tz = %sz
 = %s AND z >= %szid = %szid = %s AND rd   z
                UPDATE z
                SET z = z - %s
                WHERE z
            zSELECT z FROM z WHERE Nrc   0F)rO   r=   _metadb_tabler   r>   r<   r   cursorexecuterowcountfetchoner   r(   ra   )r   r   r   r   r   rj   
table_namerl   where_clausewhere_paramsid_fieldru   sqlrows_affectedrowrp   s                   r   r   z&WalletRepository.deduct_balance_atomic1  s   $ ..z:
((..77
 Q;"33GZHO(( **"&"<"<!=UC 'y"&"<"<!=Z
|SYZ '011H ( 'y!-j\@ '0H F"| $LJ< 0#n %C
 NN3< 78 #OOMq j\
|78*ERI oo'%c#a&k2[() ! . j\
|78*ERI oo'&-c#a&k&:O ,,W,F&-clOo-E !  s   9A>F3A(F33F<c                b    | j                  |      }| j                  |d      }t        ||      S )z=Get the current balance for a specific point type for a user.Frc   )rO   ra   re   )r   r   r   rj   rk   s        r   r   z!WalletRepository.get_user_balance  s4    ..z:
..w5.I{J//r   c                   ddl m } |j                  }t        |t              r|j	                  |      }| j
                  j                  j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  |j                  ||j                   
      }|j"                  S )z
        Create a transaction record and return the new transaction ID.
        
        Args:
            record: TransactionRecord dataclass instance
        r   )datetime)
wtypeiiduidtyper   balance
trans_typedescrcdate
extra_data)r   r   
isinstancer(   fromisoformatr:   rW   rZ   r   r   r   r   r   r   r   r   r   rd   )r   r    r   r   transaction_objs        r   r!   z*WalletRepository.create_transaction_record  s     	& eS!**51E ,,44;;,,



==NN((,,(( < 
 !!!r   )NNNr   )r?   type[Model]r@   r$   rA   type[Model] | NonerB   r   rC   zdict[str, str] | NonerD   r(   r"   )r#   r   )r#   r   )r   r(   r#   r(   r%   )r   r'   r]   r)   r&   r*   r+   r,   )r-   r.   r/   r0   rE   r   rH   rJ   rL   rO   ra   r   atomicr   r   r   r!   r   r   r   r2   r2   ^   sC   > 48+/6:$-#K#K $#K 1	#K
 )#K 4#K "#KJ! "#FM^   %,, , 	,
 , 
, ,\   %O.O. O. 	O.
 O. 
O. O.b0 " "r   r2   N)r0   
__future__r   decimalr   typingr   r   	django.dbr   r   django.db.modelsr	   r
   r   r2   r   r   r   <module>r      s8    E "  * - &Lx L^G" G"r   