Ë
    ‰hBj <  ã                  ó    — d Z ddlmZ ddlZddlZddlmZ ddlmZ ddlm	Z	m
Z
mZ ddlmZ  ej                  e«      Zdd	d
dœZ G d„ d«      Zy)zþBilling ledger: shadow/live token charging with dedup, circuit breaker, and logging.

In SHADOW mode: calculates would_charge, logs it, but never calls the airminal API.
In LIVE mode: checks balance, charges via API BEFORE the action, blocks on failure.
é    )ÚannotationsN©Údatetime)ÚPath)ÚAirminalClientÚChargeResultÚInsufficientTokensError)ÚSettingsÚtokens_per_lead_67Útokens_per_lead_68Útokens_per_lead_69)Ú
DopasowanozDo weryfikacjizBrak dopasowaniac                  ó:  — e Zd ZdZddd„Zedd„«       Zedd„«       Zedd„«       Zdd„Z		 	 	 	 	 	 	 	 dd„Z
dd„Zddd	„Zdd
„Zdd„Zd d„Zd d„Zd!d„Zd!d„Z	 	 	 	 	 	 	 	 	 	 d"d„Z	 	 	 	 	 	 	 	 	 	 d"d„Z	 	 	 	 	 	 	 	 	 	 d"d„Zd#d„Zd$d„Z	 	 	 	 	 	 	 	 	 	 	 	 	 	 d%d„Zy)&ÚBillingLedgerz7Manages token charges per cycle with shadow/live modes.c                óæ  — || _         |j                  | _        || _        |j                  | _        d| _        d| _        d | _        |j                  r%t        |j                  |j                  «      | _        |j                  }t        |«      j                  j                  dd¬«       t!        j"                  |«      | _        t         j&                  | j$                  _        | j+                  «        y )Nç        FT)ÚparentsÚexist_ok)Ú	_settingsÚbilling_modeÚ_modeÚ	_cycle_idÚbilling_max_tokens_per_cycleÚ_max_per_cycleÚ_cycle_totalÚ_haltedÚ_clientÚairminal_billing_keyr   Úairminal_billing_urlÚdb_pathr   ÚparentÚmkdirÚsqlite3ÚconnectÚ_connÚRowÚrow_factoryÚ_init_schema)ÚselfÚsettingsÚcycle_idr    s       úG/var/www/html/agent.samochodowy/src/agent_samochodowy/billing/ledger.pyÚ__init__zBillingLedger.__init__"   sÀ   € Ø!ˆŒØ×*Ñ*ˆŒ
Ø!ˆŒØ&×CÑCˆÔØˆÔØˆŒð /3ˆŒØ×(Ò(Ü)Ø×-Ñ-Ø×-Ñ-óˆDŒLð ×"Ñ"ˆÜˆW‹×Ñ×"Ñ"¨4¸$Ð"Ô?Ü—_‘_ WÓ-ˆŒ
Ü!(§¡ˆ�
‰
ÔØ×ÑÕó    c                ó    — | j                   dk(  S )NÚlive)r   ©r)   s    r,   Úis_livezBillingLedger.is_live9   s   € à�z‰z˜VÑ#Ð#r.   c                ó   — | j                   S ©N)r   r1   s    r,   ÚhaltedzBillingLedger.halted=   s   € à�|‰|Ðr.   c                ó   — | j                   S r4   )r   r1   s    r,   Úcycle_totalzBillingLedger.cycle_totalA   s   € à× Ñ Ð r.   c                ó–   — | j                   j                  }|dk  ryd|› d| j                  › �}d|dd › �}| j                  |||d«      S )	z?Charge for a group scan. Returns True if action should proceed.r   Tzscan-ú-zskan Né2   Úscan)r   Útokens_per_scanr   Ú_charge_or_log)r)   Úgroup_idÚ
group_nameÚtokensÚ	referenceÚtasks         r,   Úcharge_scanzBillingLedger.charge_scanI   s\   € à—‘×/Ñ/ˆØ�QŠ;ØØ˜H˜: Q t§~¡~Ð&6Ð7ˆ	Ø�z # 2�Ð'Ð(ˆØ×"Ñ" 6¨4°¸FÓCÐCr.   c                ó®   — t         j                  |«      }|syt        | j                  |d«      }|dk  ryd|› d|› �}d|› �}| j	                  |||d«      S )z?Charge for a lead write. Returns True if action should proceed.Tr   r   zlead-r9   zlead Úlead)Ú_LEAD_STATUS_TOKEN_FIELDÚgetÚgetattrr   r=   )r)   Ústatus_nameÚ	client_idÚpost_idÚfieldr@   rA   rB   s           r,   Úcharge_leadzBillingLedger.charge_leadR   sk   € ô )×,Ñ,¨[Ó9ˆÙØÜ˜Ÿ™¨°Ó4ˆØ�QŠ;ØØ˜I˜; a¨ yÐ1ˆ	Ø�{�mÐ$ˆØ×"Ñ" 6¨4°¸FÓCÐCr.   c                ó8  — t        j                  |«      }|j                  d«      j                  «       }|D �cg c]  }t	        |d   «      ‘Œ }}|s|j                  «        dddœS dj                  d„ |D «       «      }|j                  d|› d�|«      }|j                  «       \  }}	|j                  d	|› d�|«       |j                  «        |j                  «        t        j                  d
||	t        |«      «       |t        |	«      t        |«      dœS c c}w )zŒMark billing entries as invalid for cycles where posts_fetched=0.

        Returns summary dict with counts and tokens invalidated.
        z0SELECT id FROM cycle_log WHERE posts_fetched = 0r   r   )Úinvalidatedr@   ú,c              3  ó    K  — | ]  }d –— Œ y­w)ú?N© )Ú.0Ú_s     r,   ú	<genexpr>z7BillingLedger.invalidate_empty_scans.<locals>.<genexpr>o   s   è ø€ Ò7¨¤Ñ7ùs   ‚zNSELECT COUNT(*), COALESCE(SUM(tokens), 0) FROM billing_log WHERE cycle_id IN (z=) AND action_type = 'scan' AND result != 'invalid_empty_scan'zHUPDATE billing_log SET result = 'invalid_empty_scan' WHERE cycle_id IN (z>Invalidated %d scan charges (%.1f tokens) from %d empty cycles)rO   r@   Úempty_cycles)r#   r$   ÚexecuteÚfetchallÚstrÚcloseÚjoinÚfetchoneÚcommitÚloggerÚinfoÚlenÚfloat)
r)   r    ÚconnrW   ÚrÚ	empty_idsÚplaceholdersÚcurÚcountr@   s
             r,   Úinvalidate_empty_scansz$BillingLedger.invalidate_empty_scans`   s  € ô
 �‰˜wÓ'ˆà—|‘|Ø>ó
ç
‰(‹*ð 	ð )5Ö5 1”S˜˜1™•YÐ5ˆ	Ð5ÙØ�J‰JŒLØ#$°Ñ4Ð4à—x‘xÑ7¨YÔ7Ó7ˆà�l‰lð"Ø". ð 01ð2ð ó	
ˆð Ÿ™›‰ˆˆvð 	�‰ð"Ø". ð 01ð2ð ô		
ð 	�‰ŒØ�
‰
Œä�‰ØLØ�6œ3˜y›>ô	
ð  %´°f³ÌsÐS\Ë~Ñ^Ð^ùò; 6s   ¹Dc                ó¼  — | j                   sy	 | j                   j                  «       }|j                  |j                  z
  }||k  r:| j                  «       s)| j                  d|«       t
        j                  d||«       y| j                  «       r(| j                  d|«       t
        j                  d|«       y# t        $ r t
        j                  d«       Y yw xY w)z¿Check if org has enough tokens to proceed. Returns True if OK.

        Sets tokens_exhausted flag in DB if insufficient.
        Auto-clears flag if balance is restored (soft stop).
        Tu.   Balance check failed â€” proceeding cautiouslyu7   TOKENS EXHAUSTED: available=%.1f < min=%.1f â€” haltingFu,   TOKENS RESTORED: available=%.1f â€” resuming)r   Úcheck_balanceÚbalanceÚlockedÚ	Exceptionr_   Ú	exceptionÚ_is_tokens_exhaustedÚ_set_tokens_exhaustedÚerrorr`   )r)   Ú
min_tokensÚresultÚ	availables       r,   Úcheck_balance_sufficientz&BillingLedger.check_balance_sufficient�   sÎ   € ð �|Š|Øð	Ø—\‘\×/Ñ/Ó1ˆFØŸ™¨¯©Ñ6ˆIð
 �zÒ!Ø×,Ñ,Ô.Ø×*Ñ*¨4°Ô;Ü—‘ØMØ˜zôð ð ×$Ñ$Ô&Ø×&Ñ& u¨iÔ8Ü�K‰KØ>Øôð øô) ò 	Ü×ÑÐMÔNÙð	ús   �3B: Â:CÃCc                ó"   — | j                  «       S r4   )rp   r1   s    r,   Úis_tokens_exhaustedz!BillingLedger.is_tokens_exhausted¯   s   € Ø×(Ñ(Ó*Ð*r.   c                ó\   — | j                   j                  d«      }|j                  «       d uS )NzMSELECT 1 FROM account_guard WHERE key = 'tokens_exhausted' AND value = 'true'©r%   rX   r]   )r)   rg   s     r,   rp   z"BillingLedger._is_tokens_exhausted²   s,   € Ø�j‰j× Ñ Ø[ó
ˆð �|‰|‹~ TÐ)Ð)r.   c                óD  — ddl m } |j                  «       j                  «       }|rE| j                  j	                  d|f«       | j                  j	                  dt        |«      |f«       n| j                  j	                  d«       | j                  j                  «        y )Nr   r   zdINSERT OR REPLACE INTO account_guard (key, value, updated_at) VALUES ('tokens_exhausted', 'true', ?)zgINSERT OR REPLACE INTO account_guard (key, value, updated_at) VALUES ('tokens_exhausted_balance', ?, ?)zWDELETE FROM account_guard WHERE key IN ('tokens_exhausted', 'tokens_exhausted_balance'))r   ÚnowÚ	isoformatr%   rX   rZ   r^   )r)   Ú	exhaustedrl   Ú_dtr|   s        r,   rq   z#BillingLedger._set_tokens_exhausted¸   s‰   € Ý,Ø�g‰g‹i×!Ñ!Ó#ˆÙØ�J‰J×Ñð9Ø;>¸&ôð �J‰J×Ñð<Ü>AÀ'»lÈCÐ=Põð
 �J‰J×ÑðCôð 	�
‰
×ÑÕr.   c           	     ó  — |j                   r|j                  sy	 ddlm} ddlm} |j                  j                  d«      D �cg c]#  }|j                  «       sŒ|j                  «       ‘Œ% }}|r
d}d|d›d	�}	n	d
}d|d›d�}	|D ]9  }
 ||j                   «      }|j                   ||j                  |
||	¬«      «       Œ; t        j                  d|rdndt        |«      «       yc c}w # t        $ r t        j                  d«       Y yw xY w)z1Send email about token exhaustion or restoration.Nr   )ÚSendGridAPIClient)ÚMailrP   u.   ALARM: Agent Samochodowy â€” tokeny wyczerpanezL<h2 style='color:#dc2626'>Tokeny na koncie wyczerpane</h2><p>Saldo: <strong>z.1fu…   </strong> tokenÃ³w</p><p>Agent zostaÅ‚ <strong>automatycznie wstrzymany</strong>.</p><p>DoÅ‚aduj tokeny, aby wznowiÄ‡ skanowanie.</p>u:   INFO: Agent Samochodowy â€” wznowiono (tokeny doÅ‚adowane)uV   <h2 style='color:#16a34a'>Saldo doÅ‚adowane â€” agent wznowiony</h2><p>Saldo: <strong>uI   </strong> tokenÃ³w</p><p>Skanowanie zostaÅ‚o automatycznie wznowione.</p>)Ú
from_emailÚ	to_emailsÚsubjectÚhtml_contentz&Token %s alarm sent to %d recipient(s)r~   Úrestoredz Failed to send token alarm email)Úsendgrid_api_keyÚnotify_email_toÚsendgridr�   Úsendgrid.helpers.mailr‚   ÚsplitÚstripÚsendÚnotify_email_fromr_   r`   ra   rn   ro   )r)   r*   r~   rl   r�   r‚   ÚeÚ
recipientsr…   ÚhtmlÚ	recipientÚsgs               r,   Úsend_tokens_alarmzBillingLedger.send_tokens_alarmË   s/  € à×(Ò(°×0HÒ0HØð 	AÝ2Ý2à-5×-EÑ-E×-KÑ-KÈCÓ-PÖ^¨ÐTU×T[ÑT[ÕT]˜!Ÿ'™'�)Ð^ˆJÐ^ÙØJ�ð)Ø)0°¨ð 6GðHñ ð W�ð)Ø)0°¨ð 6JðKð ð (ò �	Ù& x×'@Ñ'@ÓA�Ø—‘™Ø'×9Ñ9Ø'Ø#Ø!%ô	õ ðô �K‰KÐ@Ù'0™°jÄ#ÀjÃ/õSùò3 _øô6 ò 	AÜ×ÑÐ?Ö@ð	Aús*   ›)C+ ÁC&ÁC&Á,A9C+ Ã&C+ Ã+DÄDc                óº   — | j                   j                  «        | j                   j                  «        | j                  r| j                  j                  «        y y r4   )r%   r^   r[   r   r1   s    r,   r[   zBillingLedger.closeñ   s>   € Ø�
‰
×ÑÔØ�
‰
×ÑÔØ�<Š<Ø�L‰L×ÑÕ ð r.   c                óÚ   — | j                   j                  d«       | j                   j                  d«       | j                   j                  d«       | j                   j                  «        y )NaÕ  
            CREATE TABLE IF NOT EXISTS billing_log (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                cycle_id TEXT,
                action_type TEXT NOT NULL,
                task TEXT NOT NULL,
                reference TEXT NOT NULL,
                tokens REAL NOT NULL,
                mode TEXT NOT NULL,
                result TEXT NOT NULL,
                balance_after REAL
            )
        z¨
            CREATE TABLE IF NOT EXISTS charged_references (
                reference TEXT PRIMARY KEY,
                charged_at TEXT NOT NULL
            )
        zÂ
            CREATE TABLE IF NOT EXISTS account_guard (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
        )r%   rX   r^   r1   s    r,   r(   zBillingLedger._init_schemaû   s]   € Ø�
‰
×Ñð ô 	ð 	�
‰
×Ñð ô 	ð 	�
‰
×Ñð ô 	ð 	�
‰
×ÑÕr.   c                ó  — | j                   r&t        j                  d||«       | j                  dk7  S | j                  |z   | j
                  kD  rXd| _         t        j                  d| j                  || j
                  «       | j                  ||||dd«       | j                  dk7  S | j                  |«      rt        j                  d|«       y| j                  r| j                  ||||«      S | j                  ||||«      S )zFUnified charge/log entry point. Returns True if action should proceed.u#   Billing HALTED â€” skipping %s (%s)r0   Tu@   BILLING CIRCUIT BREAKER: cycle total %.1f + %.1f > %.1f â€” HALTÚHALTEDNz+Billing dedup: %s already charged, skipping)r   r_   Úwarningr   r   r   rr   Ú
_log_entryÚ_is_duplicateÚdebugr2   Ú_charge_liveÚ_charge_shadow©r)   r@   rB   rA   Úaction_types        r,   r=   zBillingLedger._charge_or_log  së   € ð
 �<Š<Ü�N‰NÐ@À)ÈTÔRØ—:‘: Ñ'Ð'à×Ñ˜vÑ%¨×(;Ñ(;Ò;ØˆDŒLÜ�L‰LØRØ×!Ñ! 6¨4×+>Ñ+>ôð �O‰O˜K¨¨y¸&À(ÈDÔQØ—:‘: Ñ'Ð'ð ×Ñ˜iÔ(Ü�L‰LÐFÈ	ÔRØà�<Š<Ø×$Ñ$ V¨T°9¸kÓJÐJØ×"Ñ" 6¨4°¸KÓHÐHr.   c                óÂ   — | xj                   |z  c_         | j                  |«       | j                  ||||dd«       t        j	                  d|||| j                   «       y)z.Shadow mode: log would_charge, always proceed.Úwould_chargeNzC[SHADOW] would_charge %.1f tokens for %s (ref=%s, cycle_total=%.1f)T)r   Ú_mark_chargedr›   r_   r`   r    s        r,   rŸ   zBillingLedger._charge_shadow8  s\   € ð 	×Ò˜VÑ#ÕØ×Ñ˜9Ô%Ø�‰˜ T¨9°f¸nÈdÔSÜ�‰ØQØ�D˜) T×%6Ñ%6ô	
ð r.   c           	     óœ  — | j                   s-t        j                  d|«       | j                  ||||dd«       y	 | j                   j	                  |||«      }| xj
                  |z  c_        | j                  |«       | j                  ||||d|j                  «       t        j                  d||||j                  «       y# t        $ rJ | j                  ||||dd«       | j                  dd	«       d| _        t        j                  d
|||«       Y yt        $ r/ | j                  ||||dd«       t        j                  d|«       Y yw xY w)z:Live mode: charge via API BEFORE action. Block on failure.u8   LIVE mode but no airminal API key â€” blocking action %sÚerror_no_keyNFÚchargedz8[LIVE] charged %.1f tokens for %s (ref=%s, balance=%.1f)TÚinsufficientr   uc   [LIVE] INSUFFICIENT TOKENS for %s (%.1f needed, ref=%s) â€” setting tokens_exhausted, halting cyclerr   u?   [LIVE] airminal API error for %s â€” fail-safe: blocking action)r   r_   rr   r›   Úcharger   r¤   Úbalance_afterr`   r	   rq   r   rn   ro   )r)   r@   rB   rA   r¡   rt   s         r,   rž   zBillingLedger._charge_liveE  sG  € ð �|Š|Ü�L‰LÐSÐU^Ô_Ø�O‰O˜K¨¨y¸&À.ÐRVÔWØð	Ø—\‘\×(Ñ(¨°°yÓAˆFØ×Ò Ñ'ÕØ×Ñ˜yÔ)Ø�O‰OØ˜T 9¨fØ˜6×/Ñ/ôô �K‰KØJØ˜˜i¨×)=Ñ)=ôð øÜ&ò 		Ø�O‰O˜K¨¨y¸&À.ÐRVÔWØ×&Ñ& t¨QÔ/ØˆDŒLÜ�L‰Lð:à�f˜iôñ
 Üò 	Ø�O‰O˜K¨¨y¸&À'È4ÔPÜ×ÑØQÐS\ôñ ð	ús   »BC ÃAEÄ4EÅ
Ec                ó`   — | j                   j                  d|f«      }|j                  «       d uS )Nz4SELECT 1 FROM charged_references WHERE reference = ?rz   )r)   rA   rg   s      r,   rœ   zBillingLedger._is_duplicatep  s0   € Ø�j‰j× Ñ ØBÀYÀLó
ˆð �|‰|‹~ TÐ)Ð)r.   c                ó´   — | j                   j                  d|t        j                  «       j	                  «       f«       | j                   j                  «        y )NzNINSERT OR IGNORE INTO charged_references (reference, charged_at) VALUES (?, ?))r%   rX   r   r|   r}   r^   )r)   rA   s     r,   r¤   zBillingLedger._mark_chargedv  s@   € Ø�
‰
×ÑØ\ØœŸ™›×0Ñ0Ó2Ð3ô	
ð 	�
‰
×ÑÕr.   c                óü   — | j                   j                  dt        j                  «       j	                  «       t        | j                  «      ||||| j                  ||f	«       | j                   j                  «        y )NzÅ
            INSERT INTO billing_log
                (timestamp, cycle_id, action_type, task, reference, tokens, mode, result, balance_after)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        )	r%   rX   r   r|   r}   rZ   r   r   r^   )r)   r¡   rB   rA   r@   rt   rª   s          r,   r›   zBillingLedger._log_entry�  sk   € ð 	�
‰
×Ñð ô
 �L‰L‹N×$Ñ$Ó&Ü�—‘ÓØØØØØ�J‰JØØð

ô		ð 	�
‰
×ÑÕr.   N)r   )r*   r
   r+   ú	int | strÚreturnÚNone)r¯   Úbool)r¯   rb   )r>   rZ   r?   rZ   r¯   r±   )rI   rZ   rJ   r®   rK   rZ   r¯   r±   )r    rZ   r¯   Údict)g333333Ó?)rs   rb   r¯   r±   )r~   r±   rl   rb   r¯   r°   )r¯   r°   )
r@   rb   rB   rZ   rA   rZ   r¡   rZ   r¯   r±   )rA   rZ   r¯   r±   )rA   rZ   r¯   r°   )r¡   rZ   rB   rZ   rA   rZ   r@   rb   rt   rZ   rª   zfloat | Noner¯   r°   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r-   Úpropertyr2   r5   r7   rC   rM   ri   rv   rx   rp   rq   r•   r[   r(   r=   rŸ   rž   rœ   r¤   r›   rS   r.   r,   r   r      s…  „ ÙAôð. ò$ó ð$ð òó ðð ò!ó ð!óDðDØðDØ+4ðDØ?BðDà	óDó'_ôZ óD+ó*óó&$AóL!óðDIØðIØ#&ðIØ36ðIØEHðIà	óIð6ØðØ#&ðØ36ðØEHðà	óð%Øð%Ø#&ð%Ø36ð%ØEHð%à	ó%óV*óðàðð ðð ð	ð
 ðð ðð $ðð 
ôr.   r   )r¶   Ú
__future__r   Úloggingr#   r   Úpathlibr   Ú agent_samochodowy.billing.clientr   r   r	   Úagent_samochodowy.configr
   Ú	getLoggerr³   r_   rF   r   rS   r.   r,   ú<module>r¾      s[   ðñõ #ã Û Ý Ý ÷ñ õ
 .à	ˆ×	Ñ	˜8Ó	$€ð 'Ø*Ø,ñÐ ÷zò zr.   