Ë
    §‘ˆjÞ"  ã                   ót  — d Z ddlZddlZddlZddlmZmZm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mZmZmZ ddlmZ dd	lmZmZmZmZmZmZmZmZ  ej@                  e!«      Z" ed
dg¬«      Z#dZ$dZ%e#jM                  d«       edd¬«       edd¬«      fde'de'fd„«       Z(de'de'fd„Z)d*de'de*de	fd„Z+	 d+de'de'de'de'ddf
d „Z,e#jM                  d!«       ed«       ed«       ed«       ed«       ed«       ed«      fde'd"e'd#e'd$e'd%e'd&e'fd'„«       Z-e#jM                  d(«       ed«      fde'fd)„«       Z.y),z·Shopify OAuth endpoints.

Mounted at /api/shopify to match the redirect URL registered in the Shopify Dev
Dashboard (app version galaxiq-5). Changing this prefix breaks the callback.
é    N)Ú	APIRouterÚHTTPExceptionÚQuery)ÚRedirectResponse)Úsettings)ÚTenantHasNoUserErrorÚtrack_connectÚupsert_integration)ÚKIND_SHOPIFYÚencrypt_credentialsÚget_sourcesÚupsert_source)Úget_redis_client)ÚSHOPIFY_API_VERSIONÚShopQueryErrorÚTokenExchangeErrorÚbuild_authorize_urlÚexchange_codeÚfetch_shop_infoÚnormalise_shop_domainÚverify_hmacz/api/shopifyÚshopify)ÚprefixÚtagszshopify:oauth:iX  z/install.z<handle>.myshopify.com)Údescriptionz"GalaxiQ tenant to bind the shop toÚshopÚ	tenant_idc              ƒ   ó  K  — t        | «      }|s#t        j                  d| «       t        dd¬«      ‚t	        j
                  d«      }t        j                  ||dœ«      }t        «       }|4 ƒd{  –—†  |j                  t        › |› �t        |«      ƒ d{  –—†  ddd«      ƒd{  –—†  t        j                  d||«       t        |d	d
«       t        t        ||«      «      S 7 Œt7 ŒO7 ŒA# 1 ƒd{  –—†7  sw Y   ŒQxY w­w)a  Start the authorization code grant.

    TODO(auth): tenant_id arrives from the query string because this codebase
    has no session. That lets anyone bind a shop to any tenant. It must come
    from an authenticated session before a single external merchant connects.
    z,Rejected install for invalid shop domain: %ri�  zInvalid shop domain)Ústatus_codeÚdetailé   )r   r   Nz+Starting Shopify install for %s (tenant %s)Úoauth_authorize_url_readyÚsuccess)r   ÚloggerÚwarningr   ÚsecretsÚ	token_hexÚjsonÚdumpsr   ÚsetexÚSTATE_PREFIXÚSTATE_TTL_SECONDSÚinfoÚ_safe_trackr   r   )r   r   ÚdomainÚstateÚpayloadÚclients         ú./var/www/html/strategist-ai/app/api/shopify.pyÚinstallr4   "   sï   è ø€ ô # 4Ó(€FÙä�‰ÐEÀtÔLÜ¨Ð4IÔJÐJä×Ñ˜bÓ!€EÜ�j‰j &°yÑAÓB€GäÓ€FØ÷ Qñ QØ�l‰lœl˜^¨E¨7Ð3Ô5FÈÓP×PÐP÷Q÷ Qô ‡K�KÐ=¸vÀyÔQÜ�	Ð6¸	ÔBÜÔ/°¸Ó>Ó?Ð?ðQøØPøðQø÷ Q÷ Qñ QüsZ   ‚A.D Á0C%Á1D Á4#C+ÂC'ÂC+ÂD Â'C)Â(>D Ã'C+Ã)D Ã+C=Ã1C4Ã2C=Ã9D ÚparamsÚreturnc                  ó¦   — dt         j                  v rdnd}dj                  d„ | j                  «       D «       «      }t         j                  › |› |› �S )z¿FRONTEND_URL with the given query params appended.

    Shared by the error and success paths so the "does FRONTEND_URL already
    have a query string" check lives in exactly one place.
    ú?ú&c              3   ó0   K  — | ]  \  }}|› d |› �–— Œ y­w)Ú=N© )Ú.0ÚkÚvs      r3   ú	<genexpr>z _frontend_url.<locals>.<genexpr>F   s   è ø€ Ò;¡D A q˜�s˜!˜A˜3”ZÑ;ùs   ‚)r   ÚFRONTEND_URLÚjoinÚitems)r5   ÚsepÚquerys      r3   Ú_frontend_urlrF   ?   sL   € ð œ×-Ñ-Ñ-‰#°3€CØ�H‰HÑ;¨F¯L©L«NÔ;Ó;€EÜ×#Ñ#Ð$ S E¨%¨Ð1Ð1ó    Úreasonr   c                 ó0   — t        t        | ¬«      |¬«      S )z¹Send the merchant back to the UI with a readable reason.

    Rendering an error page instead would leave the flow untestable from the
    browser, which is where it actually runs.
    )Úshopify_error©r   )r   rF   )rH   r   s     r3   Ú_error_redirectrL   J   s   € ô Ü FÔ+¸ôFð FrG   Údelivery_statusÚoutcomeÚerror_messagec                 ór   — 	 t        | d|||«       y# t        $ r t        j                  d| d¬«       Y yw xY w)zsThe audit trail must never be the reason a connection fails or a
    merchant-facing error redirect doesn't happen.r   zConnect tracking failed for %sT)Úexc_infoN)r	   Ú	Exceptionr$   r%   )r   rM   rN   rO   s       r3   r.   r.   X   s<   € ðSÜ�i ¨O¸WÀmÕTøÜò SÜ�‰Ð7¸ÈTˆÖRðSús   ‚ ’!6µ6z	/callbackÚcoder0   ÚhmacÚ	timestampÚhostc              ƒ   óŒ  K  — t        | «      }|s!t        j                  d| «       t        d«      S t	        «       }|4 ƒd {  –—†  |r!|j                  t        › |› �«      ƒ d {  –—† nd }|r!|j                  t        › |› �«      ƒ d {  –—†  d d d «      ƒd {  –—†  s!t        j                  d|«       t        d«      S t        j                  |«      }	|	j                  d«      |k7  rNt        j                  d|	j                  d«      |«       t        |	j                  d«      dd	d
«       t        d
«      S | |||||dœj                  «       D �
�ci c]
  \  }
}|�|
|“Œ }}
}t        |«      s2t        j                  d|«       t        |	d   dd	d«       t        d«      S 	 t        ||«      ƒ d {  –—† }	 t        ||d   «      ƒ d {  –—† }t#        |	d   t$        |||d   |d   t&        |d   |d   dœd|d   i¬«       	 t)        |	d   ddt+        d|d   i«      ||d   |d   t&        |d   |d   dœ¬«       t        j1                  d||	d   «       t        |	d   dd «       t3        t5        d|¬!«      d"¬#«      S 7 �Œ;7 �Œ7 �Œù7 �Œì# 1 ƒd {  –—†7  sw Y   �ŒýxY wc c}}
w 7 Œò# t        $ r t        |	d   dd	d«       t        d«      cY S w xY w7 �Œ# t         $ r t        |	d   dd	d«       t        d«      cY S w xY w# t,        $ r9 t        j/                  d|	d   «       t        |	d   dd	d«       t        dd«      cY S w xY w­w)$Nz%Callback with invalid shop domain: %rÚinvalid_shopz-Callback with unknown or expired state for %sÚinvalid_stater   z5Callback shop mismatch: state had %s, callback had %sr   Únot_connectedÚfailedÚshop_mismatch)r   rS   r0   rT   rU   rV   z(Callback HMAC verification failed for %sÚhmac_failedÚtoken_exchange_failedÚaccess_tokenÚtoken_verification_failedÚnameÚscopeÚcurrency_codeÚprimary_domain)Úshop_domainÚ	shop_nameÚscopesÚapi_versionrc   rd   )r   ÚkindÚexternal_refÚconfigÚcredentialsr   Úoauth2)r   ÚproviderÚ	auth_typeÚaccess_token_ciphertextÚmetadataz7Tenant %s has no owning user; cannot record integrationztenant has no owning userÚno_owning_useri™  z'Connected Shopify store %s to tenant %sÚ	connectedr#   )rs   r   i3  rK   )r   r$   r%   rL   r   Úgetr+   Údeleter(   Úloadsr.   rC   r   r   r   r   r   r   r   r   r
   r   r   Úerrorr-   r   rF   )r   rS   r0   rT   rU   rV   r/   r2   ÚrawÚstoredr>   r?   r5   Ú
token_datar-   s                  r3   Úcallbackr{   b   sŽ  è ø€ ô # 4Ó(€FÙÜ�‰Ð>ÀÔEÜ˜~Ó.Ð.ô
 Ó€FØ÷ :ñ :Ù<A�F—J‘J¤, °¨wÐ7Ó8×8Ð8ÀtˆÙØ—-‘-¤< .°°Ð 8Ó9×9Ð9÷:÷ :ñ
 Ü�‰ÐFÈÔOÜ˜Ó/Ð/ä�Z‰Z˜‹_€FØ‡z�z�&Ó˜VÒ#Ü�‰ÐNØ—z‘z &Ó)¨6ô	3ä�F—J‘J˜{Ó+¨_¸hÈÔXÜ˜Ó/Ð/ð ˜d¨U¸DØ¨ñ ÷ �eƒg÷ ‘t�q˜!ð �ð �‰dð  €Fñ  ô
 �vÔÜ�‰ÐAÀ6ÔJÜ�F˜;Ñ'¨¸(ÀMÔRÜ˜}Ó-Ð-ð8Ü(¨°Ó6×6ˆ
ð<Ü$ V¨Z¸Ñ-GÓH×Hˆô Ø˜Ñ%ÜØà!Ø˜f™Ø  Ñ)Ü.Ø! /Ñ2Ø"Ð#3Ñ4ñ
ð $ Z°Ñ%?Ð@õð(6ÜØ˜[Ñ)ØØÜ$7Ø ¨NÑ!;Ð<ó%>ð  &Ø! &™\Ø$ WÑ-Ü2Ø!% oÑ!6Ø"&Ð'7Ñ"8ñõ	
ô2 ‡K�KÐ9¸6Ø�{Ñ#ô%ä��{Ñ# [°)Ô<ÜÜ 	°Ô7ÀSôJð Jð}:ùØ8ùà9ùð:ù÷ :÷ :ò :üó" ð 7ùÜò 8Ü�F˜;Ñ'¨¸(Ø+ô	-äÐ6Ó7Ò7ð8úð IúÜò <Ü�F˜;Ñ'¨¸(Ø/ô	1äÐ:Ó;Ò;ð<ûôR  ò 6ô 	�‰ÐNØ˜KÑ(ô	*ä�F˜;Ñ'¨¸(Ø/ô	1äÐ/°Ó5Ò5ð6üs  ‚?MÁI<ÁMÁJÁ$I?Á%$JÂ	JÂ
JÂMÂJÂB8MÅJÅ!A MÆ"J& Æ1J$Æ2J& Æ7K Ç	KÇ
K Ç3MÈ8K? È:AMÉ?JÊJÊMÊJÊJÊJÊMÊ$J& Ê&%KËMËKËMËK Ë%K<Ë9MË;K<Ë<MË??MÌ>MÍ MÍMz/statusc              ƒ   ó$   K  — dt        | «      iS ­w)z¤Connected sources for a tenant. Never returns credentials.

    Returns a list because a tenant may hold several sources once the HTTP API
    connector lands.
    Úsources)r   )r   s    r3   Ústatusr~   ×   s   è ø€ ð ”{ 9Ó-Ð.Ð.ùs   ‚)i/  )N)/Ú__doc__r(   Úloggingr&   Úfastapir   r   r   Úfastapi.responsesr   Úapp.core.configr   Ú"app.services.integrations.platformr   r	   r
   Úapp.services.catalog.sourcesr   r   r   r   Úapp.services.infra.redisr   Ú'app.services.integrations.shopify_oauthr   r   r   r   r   r   r   r   Ú	getLoggerÚ__name__r$   Úrouterr+   r,   rt   Ústrr4   rF   ÚintrL   r.   r{   r~   r<   rG   r3   ú<module>r�      sß  ðñó
 Û Û ç 3Ñ 3Ý .å $÷ñ ÷ó õ 6÷÷ ó ð
 
ˆ×	Ñ	˜8Ó	$€á	˜.°	¨{Ô	;€à€ØÐ ð ‡�ˆJÓá�cÐ'?Ô@Ù˜3Ð,PÔQñ@Ø
ð@àò@ó ð@ð82˜Cð 2 Có 2ñF˜Cð F¨cð FÐ<Ló Fð &*ñS˜3ð S°ð S¸sð SØ"ðSØ.2óSð ‡�ˆKÓá�d“Ù�d“Ù�t“Ù�d“Ù˜4“[Ù�d“ñqJØ
ðqJà
ðqJð ðqJð ð	qJð
 ðqJð òqJó ðqJðh ‡�ˆIÓÙ"'¨£*ñ /˜Cò /ó ñ/rG   