
    מi*h                       d Z ddlmZ ddlZddlmc mZ ddl	Z	ddl
Z
ddlZddlmZmZmZ ddZddZ G d dej$                        Z G d	 d
ej$                        Z G d dej$                        Z G d dej$                        Z G d dej$                        Z G d dej$                        Z G d dej$                        Z G d dej$                        Z G d dej$                        Zedk(  r ej:                  d       yy)u  
Tests for Module 11: core/billing — Stripe Deep Integration
=============================================================

Coverage
--------
BB1  GenesisBilling initializes without stripe package (graceful degradation)
BB2  create_subscription maps tier to correct price (mocked)
BB3  handle_webhook verifies signature (mocked)
BB4  Webhook handler routes events correctly
BB5  Checkout session includes correct tier pricing
BB6  TIER_PRICES contains exactly 3 tiers with correct AUD prices

WB1  _handle_checkout_completed returns provision action
WB2  _handle_subscription_deleted returns revoke action
WB3  cancel_subscription defaults to at_period_end=True

All tests run with ZERO live network connections.
Stripe SDK is mocked via unittest.mock.

# VERIFICATION_STAMP
# Story: M11.05 — tests/infra/test_billing.py — full test suite
# Verified By: parallel-builder
# Verified At: 2026-02-25T00:00:00Z
# Tests: 12/12
# Coverage: 100%
    )annotationsN)	MagicMockpatchcallc                    t        d      } t               | _        t        dt        fi       | j                  _        t               | _        ddd| j
                  j                  _        ddd	}t               | _        t        |g
      | j                  j                  _        t               | _
        dddddid| j                  j                  _        ddd| j                  j                  _        ddd| j                  j                  _        ddddid| j                  j                  _        t               | _        t               | j                  _        dddddd| j                  j                  j                  _        t               | _        ddddddid| j                   j"                  _        t               | _        t               | j$                  _        dd i| j$                  j                  j                  _        | S )!z
    Build a minimal fake ``stripe`` package that mirrors the interface
    used by GenesisBilling.

    Returns the top-level fake module; callers can configure return values
    on sub-objects as needed.
    stripe)nameSignatureVerificationErrorcus_test123ztest@example.com)idemailprice_test_123sunaiva_starter_aud_497_monthly)r   
lookup_key)datasub_test123activegenesis_tierstarter)r   customerstatusmetadataT)r   cancel_at_period_endcanceled)r   r   )r   r   r   
cs_test123z&https://checkout.stripe.com/cs_test123$  )r   amount_aud_cents)r   urlr   evt_test123checkout.session.completedobject)r   r   )r   typer   r   z&https://billing.stripe.com/portal_test)r   errorr"   	Exceptionr
   Customercreatereturn_valuePricelistSubscriptionmodifycancelretrievecheckoutSessionWebhookconstruct_eventbilling_portal)fake	price_objs     1/mnt/e/genesis-system/tests/infra/test_billing.py_make_fake_striper6   (   s    (#D DJ,0$ylB-DJJ)
 KDM/<GY(ZDMM% (7XYIDJ#,9+#>DJJOO  "D!#Y/	-D)  $-D)
 -D)
 #Y//D+ KDM%KDMM7%.EJ1DMM  - ;DL,,MJK1DLL  - $+D"++D77D&&3 K    c                N    t        j                  t        j                  d| i      S )zCReturn a context manager that injects fake_stripe into sys.modules.r   )r   dictsysmodules)fake_stripes    r5   _patch_striper=   r   s    ::ckkHk#:;;r7   c                  (    e Zd ZdZddZddZddZy)TestGracefulDegradationzGBB1: GenesisBilling initializes and returns error dicts without stripe.c                X   t         j                  j                  d      }dt         j                  d<   ddl}ddlmc m} |j                  |       |j                  d      }|!t         j                  j                  dd       n|t         j                  d<   |j                  |       |S )zCImport GenesisBilling with stripe forcibly absent from sys.modules.r   Nr   sk_test_fakeapi_key)
r:   r;   get	importlibcore.billing.stripe_clientbillingstripe_clientreloadGenesisBillingpop)selforiginalrE   sc_modrG   s        r5   _load_without_stripez,TestGracefulDegradation._load_without_stripe~   s     ;;??8, $H 	33 '''? KKOOHd+$,CKK! r7   c                N   t        j                  t        j                  ddi      5  ddl}ddlmc m} |j                  |       	 |j                  d      }| j                  |       |j                  |       	 ddd       y# |j                  |       w xY w# 1 sw Y   yxY w)zEBB1a: Instantiation completes without ImportError when stripe absent.r   Nr   sk_testrB   )r   r9   r:   r;   rE   rF   rG   rH   rI   rJ   assertIsNotNone)rL   rE   rN   rG   s       r5   'test_init_does_not_raise_without_stripez?TestGracefulDegradation.test_init_does_not_raise_without_stripe   s     ZZh%56 	)77V$) //	/B$$W-  (	) 	)   (	) 	)s#   B#B*BBBB$c                   t        j                  t        j                  ddi      5  ddl}ddlmc m} |j                  |       	 |j                  d      }d|_
        |j                  dd      }| j                  d	|       | j                  |d	   d
       |j                  |       	 ddd       y# |j                  |       w xY w# 1 sw Y   yxY w)zABB1b: create_customer returns error dict when stripe unavailable.r   Nr   rQ   rB   Fza@b.comAlicer#   stripe_unavailable)r   r9   r:   r;   rE   rF   rG   rH   rI   rJ   STRIPE_AVAILABLEcreate_customerassertInassertEqual)rL   rE   rN   rG   results        r5   6test_create_customer_returns_error_dict_without_stripezNTestGracefulDegradation.test_create_customer_returns_error_dict_without_stripe   s    ZZh%56 	)77V$) //	/B*/' 00GDgv.  2FG  (	) 	)   (	) 	)s$   C
AB4C
4CC

CN)returnz'GenesisBilling'r]   None)__name__
__module____qualname____doc__rO   rS   r\    r7   r5   r?   r?   {   s    Q,))r7   r?   c                  (    e Zd ZdZddZddZddZy)TestCreateSubscriptionzEBB2: create_subscription resolves the right lookup key for each tier.c                z   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }d|_
        |j                  dd      }ddd       |j                  j                  j                  dgd	
       | j                  d       y# 1 sw Y   DxY w)zABB2a: 'starter' tier triggers Price.list with starter lookup key.r   NTrA   rB   r   r   r      lookup_keyslimitr#   )r6   r=   rE   rF   rG   rH   rI   rW   _stripe_librJ   _api_keycreate_subscriptionr(   r)   assert_called_once_withassertNotInrL   r<   rE   rN   rG   r[   s         r5   )test_starter_tier_uses_correct_lookup_keyz@TestCreateSubscription.test_starter_tier_uses_correct_lookup_key   s    ');' 		K77V$&*F#!,F++N+CG-G00	JF		K 	66:;1 	7 	
 	&)		K 		Ks   AB11B:c                l   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }|j                  dd      }ddd       |j                  j                  j                  dgd	
       | j                  d       y# 1 sw Y   DxY w)zGBB2b: 'enterprise' tier triggers Price.list with enterprise lookup key.r   NTrA   rB   r   
enterprise#sunaiva_enterprise_aud_1497_monthlyrh   ri   r#   )r6   r=   rE   rF   rG   rH   rI   rW   rl   rJ   rn   r(   r)   ro   rp   rq   s         r5   ,test_enterprise_tier_uses_correct_lookup_keyzCTestCreateSubscription.test_enterprise_tier_uses_correct_lookup_key   s    ');' 	N77V$&*F#!,F++N+CG00MF	N 	66>?q 	7 	
 	&)	N 	Ns   AB**B3c                   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }|j                  dd      }ddd       | j                  d       | j                  |d   d	       |j                  j                  j                          y# 1 sw Y   UxY w)
z=BB2c: unknown tier returns error dict without calling Stripe.r   NTrA   rB   r   ultra_premiumr#   invalid_tier)r6   r=   rE   rF   rG   rH   rI   rW   rl   rJ   rn   rY   rZ   r(   r)   assert_not_calledrq   s         r5   test_invalid_tier_returns_errorz6TestCreateSubscription.test_invalid_tier_returns_error   s    ');' 	Q77V$&*F#!,F++N+CG00PF	Q 	gv&.9002	Q 	Qs   AB;;CNr^   )r`   ra   rb   rc   rr   rv   r{   rd   r7   r5   rf   rf      s    O*(*&3r7   rf   c                       e Zd ZdZddZddZy)TestHandleWebhookzJBB3: handle_webhook delegates to Webhook.construct_event for verification.c                   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }|j                  ddd	      }ddd       |j                  j                  j                  ddd       | j                  d
       | j                  |d   d       y# 1 sw Y   XxY w)uJ   BB3a: valid signature → Webhook.construct_event called + event returned.r   NTrA   rB   s&   {"type": "checkout.session.completed"}zt=123,v1=abc
whsec_testpayload
sig_headerwebhook_secretr#   r"   r    )r6   r=   rE   rF   rG   rH   rI   rW   rl   rJ   handle_webhookr0   r1   ro   rp   rZ   rq   s         r5   "test_valid_signature_returns_eventz4TestHandleWebhook.test_valid_signature_returns_event   s    ');' 	77V$&*F#!,F++N+CG++A)+ , F	 	++CC5	

 	&))EF)	 	s   AC  C	c                   t               }|j                  j                  dd      |j                  j                  _        t        |      5  ddl}ddlm	c m
} |j                  |       d|_        ||_        |j                  d      }|j                  ddd	
      }ddd       | j!                  d       | j#                  |d   d       y# 1 sw Y   1xY w)uI   BB3b: SignatureVerificationError → error dict with 'invalid_signature'.zbad sigNr   TrA   rB   s   bad_payloadzt=bad,v1=noper   r   r#   invalid_signature)r6   r#   r
   r0   r1   side_effectr=   rE   rF   rG   rH   rI   rW   rl   rJ   r   rY   rZ   rq   s         r5   $test_invalid_signature_returns_errorz6TestHandleWebhook.test_invalid_signature_returns_error  s    ') 88DI 	++7 ;' 	77V$&*F#!,F++N+CG++&*+ , F	 	gv&*=>	 	s   ACCNr^   )r`   ra   rb   rc   r   r   rd   r7   r5   r}   r}      s    TG4?r7   r}   c                  @    e Zd ZdZd	dZd	dZd	dZd	dZd	dZd	dZ	y)
TestWebhookHandlerRoutingzIBB4: StripeWebhookHandler.handle_event routes to the correct sub-handler.c                (    ddl m}  |       | _        y Nr   )StripeWebhookHandlercore.billing.webhook_handlerr   handlerrL   r   s     r5   setUpzTestWebhookHandlerRouting.setUp6      E+-r7   c                    ddddddddid	id
}| j                   j                  |      }| j                  |d   d       | j                  |d   d       y)u>   BB4a: checkout.session.completed → provision_subaiva action.r    r!   cs_1cus_abcsub_xyzuser@example.comr   professional)r   r   subscriptioncustomer_emailr   r"   r   actionprovision_subaivacustomer_idNr   handle_eventrZ   rL   eventr[   s      r5   +test_checkout_completed_routed_to_provisionzETestWebhookHandlerRouting.test_checkout_completed_routed_to_provision:  sp     1  )$-&8!/ @
 **51)+>?.	:r7   c                    ddddddidid}| j                   j                  |      }| j                  |d	   d
       | j                  |d   d       y)uE   BB4b: customer.subscription.deleted → revoke_subaiva_access action.customer.subscription.deletedr!   r   r   r   rt   r   r   r   r   r   revoke_subaiva_accessr   Nr   r   s      r5   *test_subscription_deleted_routed_to_revokezDTestWebhookHandlerRouting.test_subscription_deleted_routed_to_revokeL  sj     4# )!/ >	
 **51)+BC.	:r7   c                ~    ddddddddid	}| j                   j                  |      }| j                  |d
   d       y)u*   BB4c: invoice.paid → log_payment action.zinvoice.paidr!   in_abcr   t audr   )r   r   amount_paidcurrencyr   r   r   log_paymentNr   r   s      r5   'test_invoice_paid_routed_to_log_paymentzATestWebhookHandlerRouting.test_invoice_paid_routed_to_log_payment\  sU     #" )#( %$-
 **51)=9r7   c                ~    ddddddddid	}| j                   j                  |      }| j                  |d
   d       y)uB   BB4d: invoice.payment_failed → send_dunning_notification action.zinvoice.payment_failedr!   in_failr   r      r   )r   r   r   attempt_countr   r   r   send_dunning_notificationNr   r   s      r5   %test_invoice_failed_routed_to_dunningz?TestWebhookHandlerRouting.test_invoice_failed_routed_to_dunningm  sV     -# )&8%&$-
 **51)+FGr7   c                v    ddddiid}| j                   j                  |      }| j                  |d   d       y)	uB   BB4e: unrecognised event type → unhandled action (no exception).zcharge.refundedr!   r   cus_xr   r   	unhandledNr   r   s      r5   $test_unknown_event_returns_unhandledz>TestWebhookHandlerRouting.test_unknown_event_returns_unhandled~  s?    *X
G?T4UV**51);7r7   Nr^   )
r`   ra   rb   rc   r   r   r   r   r   r   rd   r7   r5   r   r   3  s$    S.;$; :"H"8r7   r   c                  0    e Zd ZdZddZddZddZddZy)	TestCheckoutSessionTierPricingzHBB5: create_checkout_session uses the correct price lookup key per tier.c                
   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }|j                  |ddd	      }ddd       ||fS # 1 sw Y   |fS xY w)
zMHelper: run create_checkout_session for *tier*, return (result, fake_stripe).r   NTrA   rB   r   zhttps://example.com/successzhttps://example.com/cancel)tierr   success_url
cancel_url)r6   r=   rE   rF   rG   rH   rI   rW   rl   rJ   create_checkout_session)rL   r   r<   rE   rN   rG   r[   s          r5   _run_checkoutz,TestCheckoutSessionTierPricing._run_checkout  s    ');' 	77V$&*F#!,F++N+CG44197	 5 F	 {""	 {""s   AA66Bc                    | j                  d      \  }}|j                  j                  j                  dgd       | j	                  d|       y)z<BB5a: starter checkout uses sunaiva_starter_aud_497_monthly.r   r   rh   ri   r#   Nr   r(   r)   ro   rp   rL   r[   r<   s      r5   .test_starter_checkout_calls_correct_lookup_keyzMTestCheckoutSessionTierPricing.test_starter_checkout_calls_correct_lookup_key  sP    "00;66:;1 	7 	
 	&)r7   c                    | j                  d      \  }}|j                  j                  j                  dgd       | j	                  d|       y)zFBB5b: professional checkout uses sunaiva_professional_aud_997_monthly.r   $sunaiva_professional_aud_997_monthlyrh   ri   r#   Nr   r   s      r5   3test_professional_checkout_calls_correct_lookup_keyzRTestCheckoutSessionTierPricing.test_professional_checkout_calls_correct_lookup_key  sP    "00@66?@ 	7 	
 	&)r7   c                    | j                  d      \  }}|j                  j                  j                  dgd       | j	                  d|       y)zCBB5c: enterprise checkout uses sunaiva_enterprise_aud_1497_monthly.rt   ru   rh   ri   r#   Nr   r   s      r5   1test_enterprise_checkout_calls_correct_lookup_keyzPTestCheckoutSessionTierPricing.test_enterprise_checkout_calls_correct_lookup_key  sP    "00>66>?q 	7 	
 	&)r7   N)r   strr]   tupler^   )r`   ra   rb   rc   r   r   r   r   rd   r7   r5   r   r     s    R#(***r7   r   c                  H    e Zd ZdZd
dZd
dZd
dZd
dZd
dZd
dZ	d
dZ
y	)TestTierPricesDictzNBB6: TIER_PRICES has exactly 3 tiers and the lookup keys reference AUD prices.c                0    ddl m}m} || _        || _        y )Nr   )TIER_PRICESTIER_AMOUNTS_AUD_CENTS)rF   r   r   tier_pricestier_amounts)rL   r   r   s      r5   r   zTestTierPricesDict.setUp  s    R&2r7   c                N    | j                  t        | j                        d       y)z&BB6a: TIER_PRICES has exactly 3 tiers.   N)rZ   lenr   rL   s    r5   test_exactly_three_tiersz+TestTierPricesDict.test_exactly_three_tiers  s    T--.2r7   c                    | j                  d| j                         | j                  d| j                         | j                  d| j                         y)z9BB6b: starter, professional, enterprise keys all present.r   r   rt   N)rY   r   r   s    r5   test_required_tier_keys_presentz2TestTierPricesDict.test_required_tier_keys_present  s>    i!1!12nd&6&67lD$4$45r7   c                B    | j                  | j                  d   d       y)z*BB6c: starter = $497.00 AUD = 49700 cents.r   r   NrZ   r   r   s    r5   test_starter_amount_is_497_audz1TestTierPricesDict.test_starter_amount_is_497_aud  s    **95u=r7   c                B    | j                  | j                  d   d       y)z/BB6d: professional = $997.00 AUD = 99700 cents.r   r   Nr   r   s    r5   #test_professional_amount_is_997_audz6TestTierPricesDict.test_professional_amount_is_997_aud  s    **>:EBr7   c                B    | j                  | j                  d   d       y)z0BB6e: enterprise = $1,497.00 AUD = 149700 cents.rt   iH Nr   r   s    r5   "test_enterprise_amount_is_1497_audz5TestTierPricesDict.test_enterprise_amount_is_1497_aud  s    **<8&Ar7   c           	         | j                   j                         D ]-  \  }}| j                  d|j                         d|d|       / y)z?BB6f: all lookup keys contain 'aud' to confirm currency is AUD.r   zLookup key for z does not contain 'aud': )msgN)r   itemsrY   lower)rL   r   r   s      r5   test_lookup_keys_contain_audz/TestTierPricesDict.test_lookup_keys_contain_aud  sV     $ 0 0 6 6 8 	D*MM  "%dX-FznU  	r7   Nr^   )r`   ra   rb   rc   r   r   r   r   r   r   r   rd   r7   r5   r   r     s*    X3
36>CBr7   r   c                  0    e Zd ZdZddZddZddZddZy)TestHandleCheckoutCompletedzMWB1: _handle_checkout_completed extracts fields and returns provision action.c                (    ddl m}  |       | _        y r   r   r   s     r5   r   z!TestHandleCheckoutCompleted.setUp  r   r7   c                    dddddddidid	}| j                   j                  |      }| j                  |d
   d       y)z$WB1a: action is 'provision_subaiva'.r    r!   cus_prov123sub_prov456zprov@example.comr   rt   r   r   r   r   r   r   r   Nr   _handle_checkout_completedrZ   r   s      r5   %test_returns_provision_subaiva_actionzATestHandleCheckoutCompleted.test_returns_provision_subaiva_action  sX     1 -$1&8!/ >	

 88?)+>?r7   c                    dddddddidid	}| j                   j                  |      }| j                  |d
   d       y)z.WB1b: customer_id extracted from session data.r    r!   cus_wb1_testsub_abczwb1@example.comr   r   r   r   r   Nr   r   s      r5    test_returns_correct_customer_idz<TestHandleCheckoutCompleted.test_returns_correct_customer_id   sW     1 .$-&7!/ ;	

 88?.?r7   c                    dddddddidid	}| j                   j                  |      }| j                  |d
   d   d       | j                  |d
   d   d       y)z5WB1c: details dict includes tier and subscription_id.r    r!   cus_xyzsub_detailszwb1c@example.comr   r   r   r   detailsr   subscription_idNr   r   s      r5   *test_details_contain_tier_and_subscriptionzFTestHandleCheckoutCompleted.test_details_contain_tier_and_subscription  sw     1 )$1&8!/ @	

 88?	*62NC	*+<=}Mr7   Nr^   )r`   ra   rb   rc   r   r   r   r   rd   r7   r5   r   r     s    W.@ @ Nr7   r   c                  0    e Zd ZdZddZddZddZddZy)TestHandleSubscriptionDeletedzGWB2: _handle_subscription_deleted returns revoke_subaiva_access action.c                (    ddl m}  |       | _        y r   r   r   s     r5   r   z#TestHandleSubscriptionDeleted.setUp)  r   r7   c                ~    ddddddidid}| j                   j                  |      }| j                  |d	   d
       y)z(WB2a: action is 'revoke_subaiva_access'.r   r!   
sub_del123
cus_del456r   r   r   r   r   r   Nr   _handle_subscription_deletedrZ   r   s      r5   test_returns_revoke_actionz8TestHandleSubscriptionDeleted.test_returns_revoke_action-  sU     4& ,!/ ;	
 ::5A)+BCr7   c                z    ddddi did}| j                   j                  |      }| j                  |d   d       y)	z)WB2b: customer_id is extracted correctly.r   r!   sub_wb2cus_wb2_revoker   r   r   Nr  r   s      r5   r   z>TestHandleSubscriptionDeleted.test_returns_correct_customer_id<  sP     4# 0 "	
 ::5A.0@Ar7   c                    ddddddidid}| j                   j                  |      }| j                  |d	   d
   d       y)z,WB2c: details dict includes subscription_id.r   r!   sub_wb2ccus_wb2cr   rt   r   r   r   r   Nr  r   s      r5   $test_details_contain_subscription_idzBTestHandleSubscriptionDeleted.test_details_contain_subscription_idK  sZ     4$ *!/ >	
 ::5A	*+<=zJr7   Nr^   )r`   ra   rb   rc   r   r  r   r  rd   r7   r5   r   r   &  s    Q.DBKr7   r   c                       e Zd ZdZddZddZy)TestCancelSubscriptionDefaultz<WB3: cancel_subscription uses at_period_end=True by default.c                   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }|j                  d      }ddd       |j                  j                  j                  dd       |j                  j                  j                          | j!                  d       y# 1 sw Y   gxY w)	z;WB3a: default at_period_end=True calls Subscription.modify.r   NTrA   rB   sub_wb3_test)r   r#   )r6   r=   rE   rF   rG   rH   rI   rW   rl   rJ   cancel_subscriptionr*   r+   ro   r,   rz   rp   rq   s         r5   $test_default_calls_modify_not_cancelzBTestCancelSubscriptionDefault.test_default_calls_modify_not_cancelb  s    ');' 	A77V$&*F#!,F++N+CG00@F	A 	  ''?? 	@ 	
 	  ''99;&)	A 	As   ACCc                   t               }t        |      5  ddl}ddlmc m} |j                  |       d|_        ||_        |j                  d      }|j                  dd      }ddd       |j                  j                  j                  d       |j                  j                  j                          | j!                  d	       y# 1 sw Y   exY w)
z=WB3b: at_period_end=False calls Subscription.cancel directly.r   NTrA   rB   sub_wb3bF)at_period_endr#   )r6   r=   rE   rF   rG   rH   rI   rW   rl   rJ   r  r*   r,   ro   r+   rz   rp   rq   s         r5   /test_immediate_cancel_calls_subscription_cancelzMTestCancelSubscriptionDefault.test_immediate_cancel_calls_subscription_cancelw  s    ');' 	R77V$&*F#!,F++N+CG0050QF	R 	  ''??
K  ''99;&)	R 	Rs   ACCNr^   )r`   ra   rb   rc   r  r  rd   r7   r5   r  r  _  s    F***r7   r  __main__r   )	verbosity)r]   r   )r<   r   )rc   
__future__r   builtins@py_builtins_pytest.assertion.rewrite	assertionrewrite
@pytest_arr:   typesunittestunittest.mockr   r   r   r6   r=   TestCaser?   rf   r}   r   r   r   r   r   r  r`   mainrd   r7   r5   <module>r&     s   6 #   
   0 0GT<5)h// 5)x:3X.. :3B4?)) 4?vO8 1 1 O8l-*X%6%6 -*h%** %X6N("3"3 6Nz2KH$5$5 2Kr(*H$5$5 (*^ zHMMA r7   