
    `gjE                       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 ddlmZmZ  ej                  e      Zd(dZd)dZd	d
ddZ ed       G d d             Z ed       G d d             Z ed       G d d             Z ed       G d d             Z ed       G d d             Zd*dZd+dZd,dZd-dZdd	 	 	 	 	 d.dZdd d/d!Zd0d"Z d1d#Z!d2d$Z" ed       G d% d&             Z#	 	 	 	 	 	 	 	 d3d'Z$y)4u  Surface-agnostic core for the Phase 2b terminal-billing screens.

One fetch/parse per concern, consumed identically by the CLI handler
(``cli.py::_show_billing``), the TUI JSON-RPC methods
(``tui_gateway/server.py``), and any other surface. Mirrors the proven
``agent/account_usage.py::build_credits_view`` pattern: parse the server payload
into a frozen dataclass; **fail open** — when not logged in or the portal is
unreachable, return a struct with ``logged_in=False`` and let the surface degrade
gracefully (never crash).

Money discipline: the server emits decimal STRINGS (``"142.5"``, not fixed 2dp).
We keep them as :class:`decimal.Decimal` end-to-end and only format for display.
    )annotationsN)	dataclassfield)DecimalInvalidOperation)AnyOptionalc                    | y	 t        t        |       j                               S # t        t        t
        f$ r Y yw xY w)u   Parse a server money value (decimal string) into :class:`Decimal`.

    Returns None for missing/invalid input. Never raises. Accepts str/int (and,
    defensively, float — though the server always sends strings).
    N)r   strstripr   
ValueError	TypeErrorvalues    E/root/.hermes/venv/lib/python3.12/site-packages/agent/billing_view.pyparse_moneyr       sB     }s5z'')**j)4 s   !' >>c                    | y| | j                         k(  rdt        | j                         d       S dt        | j                  t        d            d       S )u   Format a Decimal as ``$X`` / ``$X.YY`` for display.

    Whole dollars show no decimals; any fractional amount shows exactly 2dp:
    ``Decimal("142.5")`` → ``"$142.50"``, ``Decimal("100")`` → ``"$100"``,
    ``Decimal("0.01")`` → ``"$0.01"``.
    u   —$f0.01)to_integral_valueformatquantizer   r   s    r   format_moneyr   /   s]     }''))6%113S9:;;vennWV_5s;<==    zthe card on your subscriptionz%your default card saved on the portalzyour auto-reload card)subPincustomerDefault
autoRefillT)frozenc                  f    e Zd ZU ded<   ded<   dZded<   ed
d       Zedd       Zed
d	       Zy)CardInfor   brandlast4NOptional[str]resolved_viac                h    | j                   s| j                  S | j                   d| j                    S )Nu	    ····)r#   r"   selfs    r   maskedzCardInfo.maskedV   s.     zz::**Ytzzl33r   c                Z    | j                   yt        j                  | j                         S )zaHuman label for why this card was picked, or None (unknown rung /
        server too old to say).N)r%   _CARD_PROVENANCE_LABELSgetr'   s    r   
provenancezCardInfo.provenance^   s*     $&**4+<+<==r   c                X    | j                   }|r| j                   d| S | j                  S )u   The one-line card display: ``Visa ····4242 — the card on your
        subscription`` (or just the masked card when provenance is unknown).u    — )r-   r)   )r(   labels     r   displayzCardInfo.displayf   s-     /4$++eE7+E$++Er   returnr   )r2   r$   )	__name__
__module____qualname____annotations__r%   propertyr)   r-   r0    r   r   r!   r!   N   sT    JJ #'L-&4 4 > > F Fr   r!   c                  8    e Zd ZU dZded<   dZded<   dZded<   y)
MonthlyCapNOptional[Decimal]	limit_usdspent_this_month_usdFboolis_default_ceiling)r3   r4   r5   r<   r6   r=   r?   r8   r   r   r:   r:   n   s$    #'I '.2+2$$r   r:   c                  B    e Zd ZU ded<   dZded<   dZded<   dZded<   y)AutoReloadCardr   kindNr$   payment_method_idr"   r#   )r3   r4   r5   r6   rC   r"   r#   r8   r   r   rA   rA   u   s&    
I'+}+E=E=r   rA   c                  F    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	<   y)

AutoReloadFr>   enabledNr;   threshold_usdreload_to_usdOptional[AutoReloadCard]card)r3   r4   r5   rF   r6   rG   rH   rJ   r8   r   r   rE   rE   }   s-    GT'+M$+'+M$+%)D
")r   rE   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<   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ed d       Zed d       Zy)!BillingStateu   Parsed ``GET /api/billing/state`` — the overview screen's data.

    Fail-open: ``logged_in=False`` (and empty fields) when not logged in or the
    portal is unreachable.
    r>   	logged_inNr$   org_idorg_slugorg_namerolezOptional[bool]can_change_plan_rawr;   balance_usdFcli_billing_enabledr8   ztuple[Decimal, ...]charge_presetsmin_usdmax_usdOptional[CardInfo]rJ   Optional[MonthlyCap]monthly_capOptional[AutoReload]auto_reload
portal_urlerrorc                B    | j                   xs dj                         dv S )u   Deprecated/display only — a legacy OWNER/ADMIN check.

        NOT a capability check; use :attr:`can_change_plan` for gating billing
        plan-change actions.
         )OWNERADMIN)rQ   upperr'   s    r   is_adminzBillingState.is_admin   s!     		R&&(,>>>r   c                J    | j                   | j                   S | j                  S )zDServer capability when supplied; otherwise the legacy role fallback.)rR   rd   r'   s    r   can_change_planzBillingState.can_change_plan   s&     ##/+++}}r   c                6    | j                   xr | j                  S )aF  True when the UI should offer charge/auto-reload actions.

        Uses the server-granted plan-change capability (``can_change_plan``,
        which itself falls back to the legacy OWNER/ADMIN role check when the
        server omits ``canChangePlan``) AND the per-org kill-switch. This lets
        the server grant charge capability to non-OWNER/ADMIN roles (e.g.
        FINANCE_ADMIN) via ``canChangePlan``, instead of hard-coding the
        deprecated 3-role admin check. (The server still enforces; this is
        just for graying out actions the user can't take.)
        )rf   rT   r'   s    r   
can_chargezBillingState.can_charge   s     ##@(@(@@r   )r2   r>   )r3   r4   r5   __doc__r6   rN   rO   rP   rQ   rR   rS   rT   rU   rV   rW   rJ   rZ   r\   r]   r^   r7   rd   rf   rh   r8   r   r   rL   rL      s     O FM "Hm""Hm"D-*..%)K") %%*,N',!%G%!%G%#D
#(,K%,(,K%, $J$E=? ?   A Ar   rL   c                   t        | t              sy | j                  d      }| j                  d      }t        |t              rt        |t              sy | j                  d      }t        |t              sd }t	        |||      S )Nr"   r#   resolvedViar"   r#   r%   )
isinstancedictr,   r   r!   )rawr"   r#   r%   s       r   _parse_cardrp      sm    c4 GGGEGGGEuc"z%'=77=)LlC(%u<HHr   c           	         t        | t              sy t        t        | j	                  d            t        | j	                  d            t        | j	                  d                  S )NlimitUsdspentThisMonthUsdisDefaultCeiling)r<   r=   r?   )rm   rn   r:   r   r,   r>   ro   s    r   _parse_monthly_caprv      sQ    c4 cggj12(1D)EF(: ;< r   c           
        t        | t              sy t        t        | j	                  d            t        | j	                  d            t        | j	                  d            t        | j	                  d                  S )NrF   thresholdUsdreloadToUsdrJ   )rF   rG   rH   rJ   )rm   rn   rE   r>   r,   r   _parse_auto_reload_cardru   s    r   _parse_auto_reloadr{      s_    c4 SWWY'(!#''."9:!#''-"89$SWWV_5	 r   c           	     j   t        | t              sy | j                  d      }|dvry |dv rt        |      S | j                  d      }| j                  d      }| j                  d      }t        |t        |t              r|nd t        |t              r|nd t        |t              r|      S d       S )	NrB   )	canonicaldistinctnone)r}   r   )rB   paymentMethodIdr"   r#   )rB   rC   r"   r#   )rm   rn   r,   rA   r   )ro   rB   rC   r"   r#   s        r   rz   rz      s    c4 776?D44$$4(( 12GGGEGGGE/9:KS/Q+W[!%-e4!%-e	  48	 r   r]   c               z   | j                  d      }t        |t              r|ni }| j                  d      }t        |t              r|ni }g }| j                  d      xs dD ]!  }t        |      }||j	                  |       # t        d|j                  d      |j                  d      |j                  d	      |j                  d
      t        | j                  d      t              r| j                  d      ndt        | j                  d            t        | j                  d            t        |      t        |j                  d            t        |j                  d            t        | j                  d            t        | j                  d            t        | j                  d            |      S )zFMap a raw ``/api/billing/state`` JSON dict into :class:`BillingState`.orgboundschargePresetsr8   NTidslugnamerQ   canChangePlan
balanceUsdcliBillingEnabledminUsdmaxUsdrJ   
monthlyCap
autoReload)rM   rN   rO   rP   rQ   rR   rS   rT   rU   rV   rW   rJ   rZ   r\   r]   )r,   rm   rn   r   appendrL   r>   tuplerp   rv   r{   )	payloadr]   raw_orgr   
raw_boundsr   presetsitemparseds	            r   billing_state_from_payloadr      sd    kk% G%/%>'BCX&J+5j$+GZRFGO,2 #T"NN6"#
 wwt}WWV_ '++o6= KK(L 9: -@!ABW~FJJx01FJJx01V,-&w{{<'@A&w{{<'@A' r   g      .@timeoutc                F   t               }||S 	 ddlm}m}m}m}m} 	  ||       }t        |t              r|j                  d      nd}	|	r ||	      nd}
|
s	 t         |             }
t!        ||
      S # t        $ r t        dd      cY S w xY w# |$ r t        d      cY S |$ r7}t        j                  d	d
       t        dt        |            cY d}~S d}~wt        $ r' t        j                  dd
       t        dd      cY S w xY w# t        $ r d}
Y w xY w)a  Fetch + parse ``/api/billing/state``. Fail-open.

    Returns ``BillingState(logged_in=False)`` when not logged in. On a portal/HTTP
    failure, returns ``logged_in=False`` with ``error`` set so the surface can show
    a clear message rather than crashing.

    Dev override: ``HERMES_DEV_BILLING_FIXTURE`` short-circuits to a fixture so the
    card-on-file / admin / scope states are testable offline (mirrors
    ``HERMES_DEV_CREDITS_FIXTURE`` for the usage model).
    Nr   )BillingAuthErrorBillingError_absolutize_portal_urlget_billing_stateresolve_portal_base_urlFzbilling client unavailablerM   r^   r   rM   u+   billing ▸ /state fetch failed (fail-open)T)exc_infou/   billing ▸ /state unexpected error (fail-open)zcould not load billing state	portalUrlr   )_dev_fixture_billing_statehermes_cli.nous_billingr   r   r   r   r   	ExceptionrL   loggerdebugr   rm   rn   r,   _fallback_portal_urlr   )r   fixturer   r   r   r   r   r   exc
raw_portalr]   s              r   build_billing_stater      s,    )*G	Q	
 	
	S#G4 .8-F[)DJ7A'
3tJ	-.E.GHJ &g*EE1  Qe3OPPQ
  -e,, =BTRe3s8<< SFQUVe3QRRS  	J	sL   A7 	B D 7BBD&D*,CD0DDD D c                *    | j                  d       dS )z?Standard billing deep-link when the server omits ``portalUrl``./z/billing?topup=open)rstrip)bases    r   r   r   R  s    kk#233r   c                    t        j                  d      xs dj                         j                         } | syd}t	        ddddt        d	      d
t        d      t        d      t        d      ft        d      t        d      |
      }t        dd      }t        d
t        d      t        d            }| dv rt        d      S | dk(  rt        d'd
dd|S | dk(  rt        d'd
|d|S | dv rt        ddd      }t        d'd
|d|S | dv rt        d'd
||d|S | d v ri |d!d"i}t        d'd
|d|S | d#v ri |d$di}t        d'd
dd|S t        dd%|  &      S )(u  Map ``HERMES_DEV_BILLING_FIXTURE`` to a :class:`BillingState` for offline UX.

    Recognized names::

        nocard           logged in · billing on · admin · NO card on file
        card             card on file · auto-reload off
        card-autoreload  card on file · auto-reload on
        notadmin         logged in · MEMBER role (billing actions disabled)
        billing-off      logged in · admin · per-org kill-switch OFF
        logged-out       not logged in

    Returns ``None`` when the env var is unset (the real portal path runs).
    Mirrors ``HERMES_DEV_CREDITS_FIXTURE``; the usage *bar* still comes from
    ``HERMES_DEV_CREDITS_FIXTURE`` (set both to pair a bar with a billing state).
    HERMES_DEV_BILLING_FIXTUREr`   Nz2https://portal.nousresearch.com/billing?topup=openorg_acmeacmezAcme Incra   z3.40T1025505500)
rN   rO   rP   rQ   rS   rT   rU   rV   rW   r]   Visa4242)r"   r#   )rF   rG   rH   )z
logged-out
logged_out	loggedoutFr   nocard)rM   rJ   rJ   )zcard-subcard_subr   rl   )zcard-autoreloadcard_autoreload
autoreload)rM   rJ   r\   )notadminz	not-adminmemberrQ   MEMBER)zbilling-offbilling_offoffrT   z$unknown HERMES_DEV_BILLING_FIXTURE: r   r8   )	osgetenvr   lowerrn   r   r!   rE   rL   )r   portalcommonrJ   autoreload_on	_sub_cardoptss          r   r   r   \  s     II239r@@BHHJD BF!FO wt}gdmDF &/Dt73<W^_cWdeM88e,,x@d@@@v~@d@@@''6hO	EdEfEECC[d=[TZ[[22+&+&(+>d>>>447&7/7>d>>> %1UVZU[/\]]r   c                 <    t        t        j                               S )al  Fresh UUID for a user-confirmed purchase (reuse on retry of the SAME buy).

    The ``Idempotency-Key`` header is mandatory on ``POST /charge``; generate one
    per confirmed purchase and reuse it across retries so a double-submit collapses
    to a single charge. Never reuse a key across different amounts (the server
    returns 409 idempotency_conflict).
    )r   uuiduuid4r8   r   r   new_idempotency_keyr     s     tzz|r   c                  4    e Zd ZU ded<   dZded<   dZded<   y)AmountValidationr>   okNr;   amountr$   r^   )r3   r4   r5   r6   r   r^   r8   r   r   r   r     s    H $F$E=r   r   c                  | xs dj                         j                  d      j                         }t        |      }|t        dd      S |dk  rt        dd      S ||j	                  t        d            k7  rt        dd	      S |||k  rt        dd
t        |             S |||kD  rt        ddt        |             S t        d|      S )zValidate a custom charge amount against bounds + 2dp (multipleOf 0.01).

    Mirrors the server's accept/reject so the UI can give instant feedback rather
    than round-tripping a sure-to-fail charge. The server is still authoritative.
    r`   r   FzEnter a dollar amount, e.g. 100)r   r^   r   zAmount must be greater than $0r   z#Amount can't be smaller than a centzMinimum is zMaximum is T)r   r   )r   lstripr   r   r   r   r   )ro   rV   rW   cleanedr   s        r   validate_charge_amountr     s     yb!((-335G!F~50QRR{50PQQ1150UVVv/5+l7>S=T0UVVv/5+l7>S=T0UVVtF33r   )r   r   r2   r;   )r   r;   r2   r   )ro   r   r2   rX   )ro   r   r2   rY   )ro   r   r2   r[   )ro   r   r2   rI   )r   zdict[str, Any]r]   r$   r2   rL   )r   floatr2   rL   )r   r   r2   r   )r2   zOptional[BillingState]r1   )ro   r   rV   r;   rW   r;   r2   r   )%ri   
__future__r   loggingr   r   dataclassesr   r   decimalr   r   typingr   r	   	getLoggerr3   r   r   r   r+   r!   r:   rA   rE   rL   rp   rv   r{   rz   r   r   r   r   r   r   r   r8   r   r   <module>r      s   #  	  ( -  			8	$>2 .>)  $F F F> $% % % $      $* * * $5A 5A 5ApI* =A##,9##V -1 /Fd4:^D  $     4	4+46G44r   