
    `gj1                       d Z ddlmZ ddlZddlZddlZddlmZmZ ddl	m
Z
mZ  ej                  e      ZdZddZddZdd	Z ed
       G d d             Z ed
       G d d             ZddZddddZddZy)u  Shared dollar-denominated usage model for the billing/subscription surfaces.

The single source of truth behind the ``/usage`` and ``/subscription`` usage
bars (TUI + CLI). User feedback (Jun 2026): the terminal surfaces show
**dollars**, never "credits", and every usage bar must make the monthly
subscription allowance and separately-purchased top-up dollars distinctly
visible.

Data source: the NAS account-info fetch (``NousPortalAccountInfo``), whose
``paid_service_access_info`` carries the three dollar magnitudes we render
(despite the legacy ``*_credits`` field names, these are USD floats):

  - ``subscription_credits_remaining``  -> plan dollars left this month
  - ``purchased_credits_remaining``     -> top-up dollars left (rolls over)
  - ``total_usable_credits``            -> total spendable

plus ``subscription.monthly_credits`` (the plan's monthly $ allowance, the
denominator for the "% used" plan bar) and ``current_period_end`` (renewal).

Design: two SEPARATE bars (decided with the user) rather than one crammed
three-segment bar — at terminal widths three same-glyph density segments are
unreadable. The plan bar is "spent vs allowance this month" (carries % used);
the top-up bar is "money you bought, doesn't expire". Each gets full
resolution and a single fill glyph, so the bar is never ambiguous and never
relies on color.

Fail-open everywhere: any missing/non-finite field degrades to fewer bars or a
magnitudes-only view; a logged-out / unreachable portal yields
``available=False`` and the surface shows nothing.
    )annotationsN)	dataclassfield)AnyOptionalg      @c                    t        | t              st        | t        t        f      syt        |       }t	        j
                  |      r|S dS )zIReturn value as a float iff it's a real finite number (not bool/NaN/Inf).N)
isinstanceboolintfloatmathisfinite)valuefs     F/root/.hermes/venv/lib/python3.12/site-packages/agent/billing_usage.py_finiter   0   s;    %je&EeAa 1*d*    c                    d| xs ddS )zH``$X.YY`` for display. ``None`` -> ``$0.00`` (callers gate on presence).$        z,.2f )r   s    r   _fmt_usdr   8   s    d#$$r   c                r   | syddl m } t        |       j                         }|sy|j                  d      r|dd dz   n|}	 |j	                  |      }|j                  d	       d
|j                   d|j                   S # t
        $ r- 	 |j                  |dd d      }n# t
        $ r |cY cY S w xY wY bw xY w)zFormat an ISO date/timestamp as a human date, e.g. ``Jul 24, 2026``.

    Accepts ``2026-07-24``, ``2026-07-24T11:05:01.000Z``, etc. Returns the raw
    string unchanged if it can't be parsed (never raises), and ``None`` for
    empty input.
    Nr   )datetimeZz+00:00
   z%Y-%m-%dz%b z, )
r   strstripendswithfromisoformat
ValueErrorstrptimestrftimedayyear)r   r   textisodts        r   format_renewsr+   =   s     !u:D"&--"4$s)h
$C##C( kk$ "&&BGG955  	""49j9B 	K	 s6   B   	B6
B B6 B0+B6/B00B65B6T)frozenc                  b    e Zd ZU dZded<   ded<   ded<   dZded<   edd	       Zedd
       Zy)UsageBaru+  One full-resolution bar: ``spent`` of ``total``, plus a remaining figure.

    ``kind`` is ``"plan"`` (monthly allowance, shows % used) or ``"topup"``
    (purchased dollars, no denominator — ``spent`` is 0 and ``total`` ==
    ``remaining`` so it renders as a full bar of available balance).
    r   kindr   remaining_usd	total_usdr   	spent_usdc           
         | j                   dk7  s| j                  dk  ry t        dt        dt	        | j
                  | j                  z  dz                    S )Nplanr   d   )r/   r1   maxminroundr2   selfs    r   pct_usedzUsageBar.pct_usedf   sG    99$..A"51c#uT^^dnn%Ds%JKLMMr   c                |    | j                   dk  ryt        dt        d| j                  | j                   z              S )z=Fraction of the bar that should read as 'remaining' (filled).r   r   g      ?)r1   r6   r7   r0   r9   s    r   fill_fractionzUsageBar.fill_fractionl   s6     >>Q3C!3!3dnn!DEFFr   N)returnzOptional[int])r>   r   )	__name__
__module____qualname____doc____annotations__r2   propertyr;   r=   r   r   r   r.   r.   X   sN     IIuN N
 G Gr   r.   c                      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Z
ded<   dZded<   dZded<   dZded<   edd       Zy)
UsageModela  Surface-agnostic dollar usage model shared by /usage and /subscription.

    ``status`` classifies the account for copy selection:
      - ``"free"``     : no paid access / no subscription (free models only)
      - ``"low"``      : paid, but total spendable < $5 (ALERT)
      - ``"healthy"``  : paid, total spendable >= $5
      - ``"depleted"`` : paid access lost (balance exhausted)
    r
   	availablefreer   statusNOptional[str]	plan_name	renews_atrenews_displayOptional[float]subscription_remaining_usdtopup_remaining_usdtotal_spendable_usdzOptional[UsageBar]plan_bar	topup_barc                N    t        | j                  xr | j                  dkD        S )Nr   )r
   rP   r9   s    r   	has_topupzUsageModel.has_topup   s#    D,,M1I1IA1MNNr   )r>   r
   )r?   r@   rA   rB   rC   rI   rK   rL   rM   rO   rP   rQ   rR   rS   rD   rU   r   r   r   rF   rF   t   s     OFC#I}##I}#$(NM(266+//+//#'H '$(I!(O Or   rF   c                   	 | t        | dd      st        d      S t        | dd      }t        | dd      }t        | dd      }|rt        t        |dd            nd}|rt        t        |d	d            nd}|rt        t        |d
d            nd}|t        |dd      nd}|t        |dd      nd}|t        t        |dd            nd}	t        |      xs |	duxr |	dkD  }
||}n#||fD cg c]  }||	 }}|rt	        |      nd}|du rd}n|
s
|r|dkD  sd}n||t
        k  rd}nd}d}|	9|	dkD  r4|2t        dt        |	|            }t        d||	t        d|	|z
              }d}||dkD  rt        d||d      }t        d|||t        |      |||||
      S c c}w # t        $ r& t        j                  dd       t        d      cY S w xY w)zBuild a :class:`UsageModel` from a ``NousPortalAccountInfo``. Fail-open.

    Returns ``UsageModel(available=False)`` when there's no usable account info
    (logged out, no entitlement block). Never raises.
    N	logged_inFrG   paid_service_access_infosubscriptionpaid_service_accesssubscription_credits_remainingpurchased_credits_remainingtotal_usable_creditsr4   current_period_endmonthly_creditsr   depletedrH   lowhealthyr   r/   r0   r1   r2   topupT)
rG   rI   rK   rL   rM   rO   rP   rQ   rR   rS   u(   usage ▸ model build failed (fail-open)exc_info)getattrrF   r   r
   sumLOW_BALANCE_THRESHOLD_USDr6   r7   r.   r+   	Exceptionloggerdebug)account_infoaccesssubpaidsub_remainingtopup_remainingtotal_usablerK   rL   monthlyhas_subscriptiontotal_spendablevpartsrI   rR   	remainingrS   s                     r   usage_model_from_accountr{      sL   J+w|[%'P..'A4HlND9|%:DA\b0PRV WXhl[a''&2OQU"VWgkQWwwv/EtLM]a25/GC.t	@CGC!5t<UY	DGO''#'8$?@Y]	?Swd/B/RwQR{ #*O!. @R1AMQRER,1c%jtO 5=F!?QR?RF(_?X-XFF
 (,7Q;=3LCWm!<=I'!c7]#:;	H )-	&?Q+> -)	I (3'4 / /
 	
M Sd  +?$OE**+s0   F) CF) 1F$9F$=B&F) $F) ),GGg      $@timeoutc                *   t               }||S 	 ddlm}  |d      xs i j                  d      }t	        |t
              r|j                         st        d      S 	 	 ddl	}ddl
m} |j                  j                  d	
      5 }|j                  |d      j                  |       }ddd       t!              S # t        $ r t        d      cY S w xY w# 1 sw Y   /xY w# t        $ r& t"        j%                  dd       t        d      cY S w xY w)a  Fetch account-info and build the shared usage model. Fail-open.

    Dev override: ``HERMES_DEV_CREDITS_FIXTURE`` short-circuits to a fixture so
    every usage state is testable without a live account (mirrors the existing
    ``/usage`` credits-block fixture path).
    Nr   )get_provider_auth_statenousaccess_tokenFrX   )get_nous_portal_account_info   )max_workersT)force_freshr|   u)   usage ▸ portal fetch failed (fail-open)rf   )_dev_fixture_usage_modelhermes_cli.authr   getr	   r   r    rF   rk   concurrent.futureshermes_cli.nous_accountr   futuresThreadPoolExecutorsubmitresultr{   rl   rm   )r}   fixturer   tok
concurrentr   poolaccounts           r   build_usage_modelr      s    '(G+;&v.4"99.I3$.. *5

+!H22q2A 	jTkk">DkQXXahXiG	j'00  +E**+	j 	j  +@4PE**+s<   AB< &C# $C)C# <CCC C# #,DDc                    t        j                  d      xs dj                         j                         } | sy| dk(  rt	        ddd      S | dv rt	        ddd	d
ddt        dddd            S | dv r.t	        ddd	d
dddt        dddd      t        dddd      	      S | dk(  rt	        ddd	d
ddt        dddd            S | dk(  rt	        ddd	d
ddt        dddd            S y)zMap ``HERMES_DEV_CREDITS_FIXTURE`` to a usage model for offline UX work.

    Recognized names: ``free | healthy | low | topup | depleted``. Returns
    ``None`` when the env var is unset (real portal path runs).
    HERMES_DEV_CREDITS_FIXTURE NrH   T)rG   rI   rK   )rc   midrc   Plusz
2026-07-01g      ,@r4   g      4@g      @rd   )rG   rI   rK   rL   rO   rQ   rR   )re   ztop-upg      (@g      :@re   r   )	rG   rI   rK   rL   rO   rP   rQ   rR   rS   rb   g333333@g0@ra   )osgetenvr    lowerrF   r.   )names    r   r   r     s<    II239r@@BHHJDv~D4HH!!"'+ $6Y\]
 	
 """'+ $ $6Y\]G44[^_

 
	
 u}"'* #6X\]
 	
 z"'* #6X\]
 	
 r   )r   r   r>   rN   )r   rN   r>   r   )r   rJ   r>   rJ   )rn   r   r>   rF   )r}   r   r>   rF   )r>   zOptional[UsageModel])rB   
__future__r   loggingr   r   dataclassesr   r   typingr   r   	getLoggerr?   rl   rj   r   r   r+   r.   rF   r{   r   r   r   r   r   <module>r      s   > #   	 (  			8	$
   +%
66 $G G G6 $O O O4P+f +/ +L;r   