+
    \>jJ                     >   R t ^ RIt^ RIt^ RIt^ RIt^ RIt^ RIHt ^ RIH	t	 ^ RI
t
RsR t ! R R4      tR&R ltR tR	 tR
 tR'R ltR tR tR tR tR tR tR tR tR tR(R ltR)R ltR tR tR t R t!R t"R t#R t$R t%R t&R*R  lt'R! t(R" t)R# t*R$ t+R% t,R# )+u   
HEAD OR TAIL — data layer (PostgreSQL).

All money moves go through ledger_move() so every transfer is mirrored in the
dual-entry ledger. Balances and ledger are always updated in the SAME
transaction, so they can never drift.
N)ThreadedConnectionPool)RealDictCursorc                      \         f;   \        \        P                  \        P                  \        P
                  R7      s R # R # )N)dsn)_POOLr   configDB_POOL_MINDB_POOL_MAXDB_DSN     /opt/headortail/db.py	init_poolr      s-    }& 2 2
 r   c                   0   a  ] tR t^t o RtR tR tRtV tR# )conn_ctxzABorrow a pooled connection; commit on success, rollback on error.c                L    \         P                  4       V n        V P                  # N)r   getconnconn)selfs   &r   	__enter__conn_ctx.__enter__   s    MMO	yyr   c                    Vf   V P                   P                  4        MV P                   P                  4        \        P	                  V P                   4       R # r   )r   commitrollbackr   putconn)r   exc_typeexctbs   &&&&r   __exit__conn_ctx.__exit__#   s8    IIII dii r   )r   N)	__name__
__module____qualname____firstlineno____doc__r   r   __static_attributes____classdictcell__)__classdict__s   @r   r   r      s     K! !r   r   c                L   \        4       ;_uu_ 4       pVP                  \        R7      pTP                  Y;'       g    R4       VR8X  d   VP	                  4       uuRRR4       # VR8X  d   VP                  4       uuRRR4       #  RRR4       R#   + '       g   i     R# ; i)zGConvenience for simple autonomous queries. fetch in {None,'one','all'}.cursor_factoryoneNallr   )r   cursorr   executefetchonefetchall)sqlparamsfetchccurs   &&&  r   qr7   +   sn    	qhhnh5C2&E><<>	 

 E><<> 
  
s   AB&BBB#	c                 f    \         P                  ! V P                  4       4      P                  4       # r   )hashlibsha256encode	hexdigest)ps   &r   pw_hashr>   8   s     >>!((*%//11r   c                  p    \         P                   P                  \         P                  P                  4      # r   )datetimenowtimezoneutcr   r   r   rA   rA   <   s%      !2!2!6!677r   c                 <    V \         P                  ! ^4      ,           # )   )secrets	token_hex)prefixs   &r   new_idrI   @   s    G%%a(((r   c	                4    V P                  RWW4VWgV34       R# )zFRecord one dual-entry transfer. Caller manages balance columns itself.z|INSERT INTO ledger (txn_type,debit_type,debit_id,credit_type,credit_id,amount,round_no,ref) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)N)r/   )	r6   txn_type
debit_typedebit_idcredit_type	credit_idamountround_norefs	   &&&&&&&&&r   ledger_moverS   D   s'     KK	@	xi	3	 r   c            
     p   \        4       ;_uu_ 4       p V P                  4       pVP                  R4       VP                  4       ^ ,          ^ 8X  dJ   VP                  RR\        P
                  \        \        P                  4      \        P                  34       RRR4       R#   + '       g   i     R# ; i)z.Create the default admin once, if none exists.zSELECT count(*) FROM adminszKINSERT INTO admins (id,username,password_hash,balance) VALUES (%s,%s,%s,%s)adminN)	r   r.   r/   r0   r   DEFAULT_ADMIN_USERr>   DEFAULT_ADMIN_PASSDEFAULT_ADMIN_BALANCE)r5   r6   s     r   bootstrap_adminrY   P   sz    	qhhj12<<>!!KK'&33223V5Q5QS	 
s   BB$$B5	c                  B    \        R RR7      p V '       d
   V R,          # R# )z,SELECT last_round FROM game_state WHERE id=1r,   r4   
last_roundi  r7   )rows    r   load_last_roundr_   ^   s!    
:%
HC #3|--r   c                      \        R V 34       R# )z.UPDATE game_state SET last_round=%s WHERE id=1Nr]   )rnds   &r   save_last_roundrb   c   s    6?r   c                 4    \        R V \        V4      3RR7      # )z]SELECT id,username,balance FROM admins WHERE username=%s AND password_hash=%s AND active=TRUEr,   r[   r7   r>   usernamepasswords   &&r   admin_loginrh   h   s#     0)*%9 9r   c                 4    \        R V \        V4      3RR7      # )zfSELECT id,username,balance,admin_id FROM agents WHERE username=%s AND password_hash=%s AND active=TRUEr,   r[   rd   re   s   &&r   agent_loginrj   n   s#     4)*%9 9r   c                 4    \        R V \        V4      3RR7      # )zwSELECT id,username,real_balance,agent_id,shop_name FROM cashiers WHERE username=%s AND password_hash=%s AND active=TRUEr,   r[   rd   re   s   &&r   cashier_loginrl   t   s$     F)*%9 9r   c                      \        R V 3RR7      # )z?SELECT cashier_id FROM terminals WHERE token=%s AND active=TRUEr,   r[   r]   )tokens   &r   cashier_by_terminalro   z   s    NXU$ $r   c                    \        4       ;_uu_ 4       pVP                  \        R 7      pVP                  RV 34       VP	                  4       pV'       d   VR,          V8  d   RRRR/uuRRR4       # VP                  RW34       VP	                  4       '       g   RRRR	/uuRRR4       # VP                  R
W 34       VP                  RW!34       \        VRRV RW4       RR/uuRRR4       #   + '       g   i     R# ; i)r*   z1SELECT balance FROM admins WHERE id=%s FOR UPDATEbalancesuccessFerrorzInsufficient admin balanceNz1SELECT id FROM agents WHERE id=%s AND admin_id=%szNot your agentz0UPDATE admins SET balance=balance-%s WHERE id=%sz0UPDATE agents SET balance=balance+%s WHERE id=%stopup_agentrU   agentTr   r.   r   r/   r0   rS   )admin_idagent_idrP   r5   r6   as   &&&   r   rt   rt      s    	qhhnh5G(ULLNAiL6)ug/KL 
 	G(	*||~~ug/?@ 
 	FHZ[FHZ[C7HU4  
   AC03.C0,9C00D	c                    \        4       ;_uu_ 4       pVP                  \        R 7      pVP                  RV 34       VP	                  4       pV'       d   VR,          V8  d   RRRR/uuRRR4       # VP                  RW34       VP	                  4       '       g   RRRR	/uuRRR4       # VP                  R
W 34       VP                  RW!34       \        VRRV RW4       RR/uuRRR4       #   + '       g   i     R# ; i)r*   z1SELECT balance FROM agents WHERE id=%s FOR UPDATErq   rr   Frs   zInsufficient agent balanceN3SELECT id FROM cashiers WHERE id=%s AND agent_id=%sNot your cashierz0UPDATE agents SET balance=balance-%s WHERE id=%sz<UPDATE cashiers SET real_balance=real_balance+%s WHERE id=%stopup_cashierru   cashierTrv   )rx   
cashier_idrP   r5   r6   ry   s   &&&   r   r~   r~      s    	qhhnh5G(ULLNAiL6)ug/KL 
 	I*	,||~~ug/AB 
 	FHZ[R(	*C'8Y
[4  
rz   c           
          \        R 4      p \        4       ;_uu_ 4       pVP                  4       pVP                  RWA\	        V4      W034       RRR4       RRRV/#   + '       g   i     L; i  \
        P                   d
    RRRR/u # i ; i)	AzUINSERT INTO agents (id,username,password_hash,phone,admin_id) VALUES (%s,%s,%s,%s,%s)Nrr   Trx   Frs   Username already existsrI   r   r.   r/   r>   psycopg2IntegrityError)rw   rf   rg   phoneaidr5   r6   s   &&&&   r   create_agentr      s    
+C
FZZ1((*CKK* 15C  4S11 Z "" F5'+DEEFs(   A/ /AA/ A,	'A/ /BBc                "   \        R4      p \        4       ;_uu_ 4       pVP                  4       pVP                  RWQ\	        V4      WV34       RRR4       RRRV/#   + '       g   i     L; i  \
        P                   d
    RRRR	/u # i ; i)
z;Cashiers are always born under an agent (never unassigned).CzdINSERT INTO cashiers (id,username,password_hash,agent_id,shop_name,phone) VALUES (%s,%s,%s,%s,%s,%s)Nrr   Tr   Frs   r   r   )rx   rf   rg   	shop_namer   cidr5   r6   s   &&&&&   r   create_cashierr      s    
+C
FZZ1((*CKK. 18N  4s33 Z "" F5'+DEEFs(   A0 0AA0 A-	(A0 0BBc                    \        4       ;_uu_ 4       pVP                  \        R7      pVP                  RV34       VP	                  4       pV'       g   RRRR/uuRRR4       # VP                  RV34       VP	                  4       '       g   RRRR	/uuRRR4       # VP                  R
V34       VP	                  4       R,          ^ 8  d   RRRR/uuRRR4       # VR,          pWb8X  d   RRRR/uuRRR4       # VP                  RW!34       VP                  RWW%R,          V 34       \        VRRVRVVR,          RV,           R7       RRR\        VR,          4      /uuRRR4       #   + '       g   i     R# ; i)z
Move a cashier to a new agent. Allowed ONLY when no shift is open.
Cashier keeps their balance (logged as a transfer). Past bets keep their
original agent_id (history pins to the old agent). Future bets use the new.
r*   zASELECT agent_id,real_balance FROM cashiers WHERE id=%s FOR UPDATErr   Frs   zCashier not foundNz!SELECT id FROM agents WHERE id=%szTarget agent not foundzFSELECT count(*) AS n FROM shifts WHERE cashier_id=%s AND status='open'nz,Close the cashier's shift before reassigningrx   zAlready managed by that agentz+UPDATE cashiers SET agent_id=%s WHERE id=%szyINSERT INTO cashier_reassignments (cashier_id,from_agent_id,to_agent_id,balance_at_move,admin_id) VALUES (%s,%s,%s,%s,%s)real_balancereassign_transferru   zcashier )rR   Tmoved_balancer   r.   r   r/   r0   rS   float)rw   r   to_agent_idr5   r6   ca
from_agents   &&&    r   reassign_cashierr      sf    
qhhnh5WM	#\\^ug/BC 
 	7+H||~~ug/GH 
 	\M	#<<>#"ug/]^ 
 
^
$ug/NO! 
" 	AKC\]& [^2DhO		
 	C,gz7K~&J,C	E4%>8J2KL5 
s+   AE,%(E,E,4E,E,<A%E,,E=	c           	         R P                  R \        ^4       4       4      p\        4       ;_uu_ 4       pVP                  4       pVP	                  RW34       VP                  4       '       g   RRRR/uuRRR4       # VP	                  R\        P                  ! VP                  4       4      P                  4       W34       RRR4       RR	R
V/#   + '       g   i     L; i) c              3   N   "   T F  p\         P                  ! R 4      x  K  	  R# 5i) ABCDEFGHJKLMNPQRSTUVWXYZ23456789N)rF   choice).0_s   & r   	<genexpr>&gen_activation_code.<locals>.<genexpr>   s     Xx!7>>"DEExs   #%r|   rr   Frs   r}   NzNINSERT INTO activation_codes (code_hash,cashier_id,agent_id) VALUES (%s,%s,%s)Tcode)
joinranger   r.   r/   r0   r9   r:   r;   r<   )rx   r   r   r5   r6   s   &&   r   gen_activation_coder      s    77XuUVxXXD	qhhjI*	,||~~ug/AB 
 	\^^DKKM*446
M	
 
 tVT** 
s   ?C>ACC 	c           	         \         P                  ! V P                  4       4      P                  4       p\	        4       ;_uu_ 4       pVP                  \        R 7      pVP                  RV34       VP                  4       pV'       g   RRRR/uuRRR4       # VP                  RVR,          34       \        P                  ! ^ 4      pVP                  R	WTR
,          34       RRRVR
VR
,          /uuRRR4       #   + '       g   i     R# ; i)r*   zLSELECT id,cashier_id FROM activation_codes WHERE code_hash=%s AND used=FALSErr   Frs   zInvalid or used codeNz1UPDATE activation_codes SET used=TRUE WHERE id=%sidz7INSERT INTO terminals (token,cashier_id) VALUES (%s,%s)r   Trn   )r9   r:   r;   r<   r   r.   r   r/   r0   rF   rG   )r   hr5   r6   r^   rn   s   &     r   activate_terminalr      s    t{{}%//1A	qhhnh5 %'(d	,llnug/EF 
 	G#d)V!!"%M-.	04%s<?PQ 
s   AC:AC::D	c                   \        4       ;_uu_ 4       pVP                  \        R7      pVP                  RV 34       VP	                  4       pV'       d   VR,          V8  d   RRRR/uuRRR4       # VP                  R	V34       VP	                  4       '       g   VP                  R
W34       VP                  RW 34       VP                  RW!34       \        VRRV RW4       VP                  RV34       VP	                  4       pRRR\        VR,          4      R\        VR,          4      /uuRRR4       #   + '       g   i     R# ; i)z@Cashier funds a player's REAL balance from the cashier's drawer.r*   z8SELECT real_balance FROM cashiers WHERE id=%s FOR UPDATEr   rr   Frs   zInsufficient cashier balanceNz*SELECT id FROM player_sessions WHERE id=%szIINSERT INTO player_sessions (id,cashier_id,real_balance) VALUES (%s,%s,0)z<UPDATE cashiers SET real_balance=real_balance-%s WHERE id=%szCUPDATE player_sessions SET real_balance=real_balance+%s WHERE id=%sfund_playerr   playerzASELECT real_balance,fund_balance FROM player_sessions WHERE id=%sTfund_balancer   )r   
session_idrP   r5   r6   r   rs   &&&    r   r   r     s    	qhhnh5NQ[P]^\\^R'&0ug/MN 
 	@:-P||~~KK +-7,DFR(	*Y(	*C	:x\WM	#LLN4q7H1Ia&7 8:% 
s   AD:3(D:BD::E	c                  T    \        R RR7      p V '       d   \        V R,          4      # R# )%SELECT amount FROM jackpot WHERE id=1r,   r[   rP           r7   r   r   s    r   get_jackpotr     s%    	
1?A!"58++r   c                     \        RV 3RR7      # )z-Return the cashier's open shift row, or None.z~SELECT id,start_time,start_balance,agent_id FROM shifts WHERE cashier_id=%s AND status='open' ORDER BY start_time DESC LIMIT 1r,   r[   r]   )r   s   &r   current_shiftr   #  s     V]%) )r   c           
        \        V 4      pV'       d   RRRVR,          RVR,          RR/# \        4       ;_uu_ 4       pVP                  \        R7      pVP	                  RV 34       VP                  4       pV'       g   RR	R
R/uuRRR4       # VP	                  RWR,          VR,          34       VP                  4       pRRRVR,          RVR,          RR	/uuRRR4       #   + '       g   i     R# ; i)zBOpen a shift if none is open (idempotent). Returns the open shift.rr   Tshift_idr   
start_timereusedr*   z6SELECT real_balance,agent_id FROM cashiers WHERE id=%sFrs   zUnknown cashierNznINSERT INTO shifts (cashier_id,agent_id,start_balance,status) VALUES (%s,%s,%s,'open') RETURNING id,start_timerx   r   )r   r   r.   r   r/   r0   )r   existingr5   r6   cir^   s   &     r   
open_shiftr   *  s    Z(H4Xd^h|4hF 	F	qhhnh5Lzm\\\^ug/@A 
 	 GJN1CD	F lln4SYc,/5B 
s   AC$A	C$$C5	c           
        \        4       ;_uu_ 4       pVP                  \        R7      pVP                  RV 34       VP	                  4       pV'       g   RRRR/uuRRR4       # VP                  RV 34       VP	                  4       R	,          pVP                  R
WCR,          34       RRRVR,          R\        V4      /uuRRR4       #   + '       g   i     R# ; i)z<Close the cashier's open shift, stamping end balance + time.r*   z\SELECT id FROM shifts WHERE cashier_id=%s AND status='open' ORDER BY start_time DESC LIMIT 1rr   Frs   zNo open shiftNz-SELECT real_balance FROM cashiers WHERE id=%sr   zKUPDATE shifts SET status='closed',end_balance=%s,end_time=now() WHERE id=%sr   Tr   end_balance)r   r.   r   r/   r0   r   )r   r5   r6   shbals   &    r   close_shiftr   >  s    	qhhnh5 79C	G\\^ug? 
 	Cj]Slln^, "$'D?	44RX}eCjQ 
s   AC%ACC	c                  T    \        R RR7      p V '       d   \        V R,          4      # R# )#SELECT profit FROM house WHERE id=1r,   r[   profitr   r   r   s    r   get_house_profitr   N  s%    	
/u=A!"58++r   c                N    V P                  RV 2V34       V P                  4       # )zIAggregate one cashier's activity since the given moment (default: today).a  SELECT
              COALESCE(SUM(CASE WHEN status<>'cancelled' THEN stake END),0) AS staked,
              COUNT(*) FILTER (WHERE status<>'cancelled')                    AS bets,
              COALESCE(SUM(CASE WHEN redeemed THEN win_amount END),0)        AS paid,
              COUNT(*) FILTER (WHERE status='win')                           AS wins
            FROM bets
            WHERE cashier_id=%s AND created_at >= )r/   r0   )r6   r   	since_sqls   &&&r   _cashier_statsr   T  s5    KK3 4=+	A 
 <<>r   c                   \        4       ;_uu_ 4       pVP                  \        R7      pVP                  RV 34       VP	                  4       p. pV F  p\        W%R,          4      p\        VR,          4      p\        VR,          4      pTP                  RVR,          RVR,          R\        VR,          4      R	VR
,          ;'       g    RRVR,          ;'       g    RRVR,          R\        VR,          4      RVRVR\        VR,          4      R\        Wx,
          ^4      /4       K  	  VuuRRR4       #   + '       g   i     R# ; i)zBAll cashiers under an agent, each with today's activity + balance.r*   zuSELECT id,username,real_balance,shop_name,phone,active,created_at FROM cashiers WHERE agent_id=%s ORDER BY created_atr   stakedpaidrf   rq   r   shopr   r   r   active
bets_todaybetsstaked_today
paid_today
wins_todaywinsprofit_todayN)
r   r.   r   r/   r1   r   r   appendintround)	rx   r5   r6   rowsoutr   sr   r   s	   &        r   agent_cashiersr   b  s   	qhhnh5 JLT;	X||~AsdG,A1X;'Fai0@JJagz1Z=5>!23VQ{^=Q=Qr7))r8Qx[c!F)nnfdL#ai.fmQ 7   # 
s   C	D8AD88E		c                   \        4       ;_uu_ 4       pVP                  \        R7      pVP                  RV 34       VP	                  4       pVP                  RV 34       VP	                  4       pRV'       d
   VR,          MRRV'       d   \        VR,          4      MRR\        VR	,          4      R
\        VR,          4      /uuRRR4       #   + '       g   i     R# ; i)zEAgent's own balance + rolled-up totals across their cashiers (today).r*   z/SELECT username,balance FROM agents WHERE id=%szRSELECT COALESCE(SUM(real_balance),0) s, COUNT(*) n FROM cashiers WHERE agent_id=%srf   r   rq   r   cashier_countr   cashier_float_totalr   N)r   r.   r   r/   r0   r   r   )rx   r5   r6   ry   aggs   &    r   agent_overviewr   x  s    	qhhnh5E{SLLN 68@{	Dlln*auQy\*SSS]!5S?	
 
s   A8C8CC 	c                P   \        4       ;_uu_ 4       pVP                  \        R7      pVP                  RV 34       VP	                  4       p. pV EF-  pVP                  RVR,          34       VP	                  4        Uu. uF  qfR,          NK  	  ppR;r^ p
V FW  p\        W+4      pV\        VR,          4      ,          q\        VR,          4      ,          q\        VR,          4      ,          p
KY  	  TP                  RVR,          R	VR	,          R
\        VR
,          4      RVR,          ;'       g    RRVR,          R\        V4      RV
R\        V^4      R\        V	^4      R\        W,
          ^4      /
4       EK0  	  VuuRRR4       # u upi   + '       g   i     R# ; i)zLAll agents under an admin, each with balance, cashier count, today's profit.r*   zdSELECT id,username,balance,phone,active,created_at FROM agents WHERE admin_id=%s ORDER BY created_atz)SELECT id FROM cashiers WHERE agent_id=%sr   r   r   r   r   rf   rq   r   r   r   r   r   r   r   r   N)r   r.   r   r/   r1   r   r   r   r   lenr   )rw   r5   r6   agentsr   ry   r   cidsr   r   r   r   r   s   &            r   admin_agentsr     sm   	qhhnh5 HJR	VAKKCagZP%(\\^4^dGG^D4F"3,%(,,eAfI6F.FdPSTUV\T]P^H^  JJagz1Z=)U1Y<EX7))r8Qx[TL$fa 0,dAfmQ 7   ) 
 5 
s&   A.FF
B#F6AFFF%	c                     \        RRR7      # )zGFlat list of every cashier with its agent, for the reassignment picker.zSELECT c.id,c.username,c.real_balance,c.shop_name,c.agent_id,a.username AS agent_name FROM cashiers c JOIN agents a ON a.id=c.agent_id ORDER BY a.username,c.usernamer-   r[   r]   r   r   r   admin_all_cashiersr     s     .5:< <r   c                    \        4       ;_uu_ 4       p V P                  \        R7      pVP                  R4       VP	                  4       pVP                  R4       VP	                  4       pVP                  R4       VP	                  4       p\        VR,          4      p\        VR,          4      pVP                  R4       VP	                  4       pVP                  R4       VP	                  4       pR	\        VR
,          4      R\        VR,          4      R\        VR
,          4      R\        VR,          4      R\        VR,          4      R\        V^4      R\        V^4      R\        WV,
          ^4      RV'       d   \        VR,          4      MRRV'       d   \        VR,          4      MR/
uuRRR4       #   + '       g   i     R# ; i)z*Top-line totals across the whole platform.r*   z9SELECT COALESCE(SUM(balance),0) s, COUNT(*) n FROM agentsz@SELECT COALESCE(SUM(real_balance),0) s, COUNT(*) n FROM cashierszSELECT COALESCE(SUM(CASE WHEN status<>'cancelled' THEN stake END),0) staked, COALESCE(SUM(CASE WHEN redeemed THEN win_amount END),0) paid, COUNT(*) FILTER (WHERE status<>'cancelled') bets FROM bets WHERE created_at >= CURRENT_DATEr   r   r   r   agent_countr   agent_balance_totalr   r   r   r   r   r   r   gross_todayhouse_profitr   r   jackpotrP   N)r   r.   r   r/   r0   r   r   r   )	r5   r6   agr   todayr   r   hpjps	            r   admin_overviewr     sP   	qhhnh5OP\\^VW\\^ A 	B uX'eFm0D9:\\^;<\\^3r#w<)>bgSC\+@%3.#eFm,neFA>N%a.-v}a9P2E"X,/3buR\*c
! 
s   FF;F;;G	)NN)Nr   )r   )r   r   )CURRENT_DATE)-r%   r9   rF   r@   jsonr   psycopg2.poolr   psycopg2.extrasr   r   r   r   r   r7   r>   rA   rI   rS   rY   r_   rb   rh   rj   rl   ro   rt   r~   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r      s         0 * 
! !	28).
@
999$!"!$FF  MF+R":0,)B(R ,,
"2<
r   