Ë
    §‘ˆjh  ã            	       ó  — d Z ddlZddlZddlZddlZddlZddlmZ ddlZddl	m
Z
 ddlmZ  ej                  e«      ZdZdZdZd	eeef   fd
„Z ej,                  d«      Zdedz  d	efd„Zdedz  d	edz  fd„Zd	efd„Zdeded	efd„ZdZded	efd„Zded	efd„Z ded	efd„Z!dZ" G d„ de#«      Z$ G d„ de#«      Z%ejL                  d„ «       Z'ddœded ed!ejP                  dz  d	efd"„Z)ddœded#ed!ejP                  dz  d	efd$„Z*y)%z°Pure functions for the Shopify OAuth authorization code grant.

No database access and no global state, so every security control here is
unit-testable without infrastructure.
é    N)Ú	urlencode)Úsettings)Úget_platform_configz2026-07zread_products,read_inventoryÚshopifyÚreturnc                  ó  — 	 t        t        «      } | r,| j                  d«      r| j                  d«      r
| d   | d   fS t        j                  t        j                  fS # t        $ r t        j	                  dd¬«       d} Y Œrw xY w)aŒ  Client credentials from platform_configs, falling back to the environment.

    platform_configs is where the other thirteen providers keep theirs, and
    rotating a secret in one place every service reads beats rotating a copy per
    service. The fallback exists so a missing row -- or an unreachable master
    database -- cannot break a working connector; do not delete it as dead code.
    z=platform_configs lookup failed; using environment credentialsT)Úexc_infoNÚ	client_idÚclient_secret)	r   ÚPLATFORMÚ	ExceptionÚloggerÚwarningÚgetr   ÚSHOPIFY_CLIENT_IDÚSHOPIFY_CLIENT_SECRET)Úcfgs    úF/var/www/html/strategist-ai/app/services/integrations/shopify_oauth.pyÚshopify_credentialsr      s‡   € ðÜ!¤(Ó+ˆñ ˆs�w‰w�{Ô#¨¯©°Ô(@Ø�;Ñ  _Ñ!5Ð5Ð5Ü×%Ñ%¤x×'EÑ'EÐEÐEøô ò Ü�‰ð %Ø/3ð 	ô 	5àŠðús   ‚A Á"BÂBz,\A[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com\ZÚshopc                 óV   — | rt        | t        «      syt        j                  | «      duS )z:True only for a well-formed <handle>.myshopify.com domain.FN)Ú
isinstanceÚstrÚSHOP_REÚmatch)r   s    r   Úis_valid_shop_domainr   4   s%   € á”z $¬Ô,ØÜ�=‰=˜Ó dÐ*Ð*ó    Úrawc                 ó
  — | rt        | t        «      sy| j                  «       j                  «       }t	        j
                  dd|«      }|j                  d«      d   }|sy|j                  d«      s|› d�}t        |«      r|S dS )a  Turn what a merchant typed into a valid shop domain, or None.

    Merchants routinely enter their custom domain or a full admin URL. A bare
    handle gets the suffix appended; anything that still fails validation
    returns None rather than a guess.
    Nz
^https?://Ú ú/r   z.myshopify.com)	r   r   ÚstripÚlowerÚreÚsubÚsplitÚendswithr   )r   Údomains     r   Únormalise_shop_domainr)   ;   s}   € ñ ”j ¤cÔ*ØØ�Y‰Y‹[×ÑÓ €FÜ�V‰V�M 2 vÓ.€FØ�\‰\˜#Ó˜qÑ!€FÙØØ�?‰?Ð+Ô,Ø�8˜>Ð*ˆÜ)¨&Ô1ˆ6Ð;°tÐ;r   c                  óF   — t         j                  j                  d«      › d�S )z=The callback URL registered with Shopify. Must match exactly.r!   z/api/shopify/callback)r   ÚPUBLIC_BASE_URLÚrstrip© r   r   Úredirect_urir.   N   s"   € ä×&Ñ&×-Ñ-¨cÓ2Ð3Ð3HÐIÐIr   Ústatec                 ód   — t        «       \  }}|t        t        «       |dœ}d| › dt        |«      › �S )zÓAuthorize URL for the offline-token authorization code grant.

    grant_options[] is deliberately omitted: including it as "per-user" yields
    an online token that dies with the merchant's admin session.
    )r
   Úscoper.   r/   úhttps://z/admin/oauth/authorize?)r   ÚSHOPIFY_SCOPESr.   r   )r   r/   r
   Ú_Úparamss        r   Úbuild_authorize_urlr6   S   sA   € ô 'Ó(�L€IˆqàÜÜ$›Øñ	€Fð �d�VÐ2´9¸VÓ3DÐ2EÐFÐFr   )ÚhmacÚ	signatureÚvaluec                 óx   — t        | «      j                  dd«      j                  dd«      j                  dd«      S )Nú%z%25ú&z%26Ú=z%3D)r   Úreplace)r9   s    r   Ú_escaper?   f   s4   € ô 	ˆE‹
ß	‰��eÓ	ß	‰��eÓ	ß	‰��eÓ	ð	r   r5   c           	      óÄ   — t        | j                  «       «      D ��cg c]&  \  }}|t        vrt        |«      › dt        |«      › �‘Œ( }}}dj	                  |«      S c c}}w )zDCanonical message Shopify signs: sorted key=value pairs joined by &.r=   r<   )ÚsortedÚitemsÚ_HMAC_EXCLUDEDr?   Újoin)r5   ÚkÚvÚpairss       r   Úbuild_hmac_messagerH   q   s`   € ô ˜6Ÿ<™<›>Ó*÷áˆAˆqØ”NÑ"ô �1‹:ˆ,�aœ ›
�|Ò$ð€Eñ ð
 �8‰8�E‹?Ðùós   �+Ac                 óP  — | j                  d«      }|syt        «       \  }}t        j                  |j	                  d«      t        | «      j	                  d«      t        j                  «      j                  «       }	 t        j                  ||«      S # t        t        f$ r Y yw xY w)zFTiming-safe verification of Shopify's signature over the query params.r7   Fzutf-8)r   r   r7   ÚnewÚencoderH   ÚhashlibÚsha256Ú	hexdigestÚcompare_digestÚ	TypeErrorÚ
ValueError)r5   Úreceivedr4   r   Údigests        r   Úverify_hmacrT   {   s–   € à�z‰z˜&Ó!€HÙØä*Ó,Ñ€A€}Ü�X‰XØ×Ñ˜WÓ%Ü˜6Ó"×)Ñ)¨'Ó2Ü�‰ó÷ �iƒkð	 ðÜ×"Ñ" 6¨8Ó4Ð4øÜ”zÐ"ò ñ ðús   Á=B ÂB%Â$B%z4{ shop { name currencyCode primaryDomain { url } } }c                   ó   — e Zd ZdZy)ÚTokenExchangeErrorz?Shopify refused to exchange the authorization code for a token.N©Ú__name__Ú
__module__Ú__qualname__Ú__doc__r-   r   r   rV   rV   “   s   „ ÚIr   rV   c                   ó   — e Zd ZdZy)ÚShopQueryErrorz6The new token could not perform a real Admin API call.NrW   r-   r   r   r]   r]   —   s   „ Ú@r   r]   c                ó¶   K  — | �| ­–— yt        j                  d¬«      4 ƒd{  –—† }|­–— ddd«      ƒd{  –—†  y7 Œ7 Œ# 1 ƒd{  –—†7  sw Y   yxY w­w)z<Use the caller's client when injected (tests), else own one.Ng      4@)Útimeout)ÚhttpxÚAsyncClient)ÚclientÚowneds     r   Ú_httprd   ›   sW   è ø€ ð ÐØŒä×$Ñ$¨TÔ2÷ 	ð 	°eØ‹K÷	÷ 	ñ 	øð 	ø÷ 	÷ 	ñ 	üsA   ‚#A¥A ¦A©A¯AºA»AÁAÁAÁ
AÁAÁA)rb   Úcoderb   c          	   ƒ   ó0  K  — t        «       \  }}t        |«      4 ƒd{  –—† }|j                  d| › d�ddi|||dœ¬«      ƒ d{  –—† }ddd«      ƒd{  –—†  j                  dk7  r9t        j                  d	| |j                  «       t        d
|j                  › �«      ‚|j                  «       }|j                  d«      }|s!t        j                  d| «       t        d«      ‚||j                  dd«      dœS 7 ŒÛ7 Œ·7 Œ©# 1 ƒd{  –—†7  sw Y   Œ¹xY w­w)zBTrade the authorization code for a permanent offline access token.Nr2   z/admin/oauth/access_tokenúContent-Typeúapplication/json)r
   r   re   ©ÚheadersÚjsonéÈ   z-Shopify token exchange failed for %s: HTTP %súHTTP Úaccess_tokenz6Shopify token exchange for %s returned no access_tokenzno access_token in responser1   r    )rn   r1   )	r   rd   ÚpostÚstatus_coder   ÚerrorrV   rk   r   )	r   re   rb   r
   r   ÚcÚrespÚdataÚtokens	            r   Úexchange_coderv   ¥   s  è ø€ ä2Ó4Ñ€Iˆ}Ü�V‹}÷ 	
ð 	
 Ø—V‘VØ�t�fÐ5Ð6Ø#Ð%7Ð8à&Ø!.Øñð ó 
÷ 
ˆ÷	
÷ 	
ð ×Ñ˜3Òä�‰ÐDØ˜4×+Ñ+ô	-ä  5¨×)9Ñ)9Ð(:Ð!;Ó<Ð<à�9‰9‹;€DØ�H‰H�^Ó$€EÙÜ�‰ÐMÈtÔTÜ Ð!>Ó?Ð?à!¨D¯H©H°W¸bÓ,AÑBÐBð/	
øð
øð	
ø÷ 	
÷ 	
ñ 	
üsW   ‚DŸC; D£"DÁC=ÁDÁ
DÁC?ÁB&DÃ=DÃ?DÄDÄD
ÄDÄDru   c             ƒ   ó  K  — t        |«      4 ƒd{  –—† }|j                  d| › dt        › d�|ddœdt        i¬«      ƒ d{  –—† }ddd«      ƒd{  –—†  j                  d	k7  r9t
        j                  d
| |j                  «       t        d|j                  › �«      ‚|j                  «       }|j                  d«      r1t
        j                  d| |d   «       t        t        |d   «      «      ‚|j                  d«      xs i j                  d«      xs i }|st        d«      ‚|j                  d«      |j                  d«      |j                  d«      xs i j                  d«      dœS 7 �ŒZ7 �Œ-7 �Œ # 1 ƒd{  –—†7  sw Y   �Œ1xY w­w)zÝProve the token works, and collect what the catalog sync will need.

    Currency and primary domain are required later for price normalisation and
    product URL construction, and are free to fetch at install time.
    Nr2   z/admin/api/z/graphql.jsonrh   )zX-Shopify-Access-Tokenrg   Úqueryri   rl   z!Shop query failed for %s: HTTP %srm   Úerrorsz-Shop query returned GraphQL errors for %s: %srt   r   zempty shop nodeÚnameÚcurrencyCodeÚprimaryDomainÚurl)rz   Úcurrency_codeÚprimary_domain)rd   ro   ÚSHOPIFY_API_VERSIONÚ_SHOP_QUERYrp   r   rq   r]   rk   r   r   )r   ru   rb   rr   rs   ÚbodyÚ	shop_nodes          r   Úfetch_shop_infor„   Â   s|  è ø€ ô �V‹}÷ 
ð 
 Ø—V‘VØ�t�f˜KÔ(;Ð'<¸MÐJà*/Ø 2ñð œ;Ð'ð ó 
÷ 
ˆ÷
÷ 
ð ×Ñ˜3ÒÜ�‰Ð8¸$À×@PÑ@PÔQÜ˜u T×%5Ñ%5Ð$6Ð7Ó8Ð8à�9‰9‹;€DØ‡x�x�ÔÜ�‰ÐDØ˜4 ™>ô	+äœS  h¡Ó0Ó1Ð1à—‘˜&Ó!Ò' R×,Ñ,¨VÓ4Ò:¸€IÙÜÐ.Ó/Ð/ð —‘˜fÓ%Ø"Ÿ™ ~Ó6Ø$Ÿ=™=¨Ó9Ò?¸R×DÑDÀUÓKñð ð1
ùð
ùð
ù÷ 
÷ 
ò 
üsW   ‚F’E,“F–,E5ÁE/ÁE5ÁFÁE2ÁDFÅ/E5Å2FÅ5FÅ;E>Å<FÆF)+r[   Ú
contextlibrL   r7   Úloggingr$   Úurllib.parser   r`   Úapp.core.configr   Ú"app.services.integrations.platformr   Ú	getLoggerrX   r   r€   r3   r   Útupler   r   Úcompiler   Úboolr   r)   r.   r6   rC   r?   ÚdictrH   rT   r�   r   rV   r]   Úasynccontextmanagerrd   ra   rv   r„   r-   r   r   ú<module>r�      s¾  ðñó
 Û Û Û Û 	Ý "ã å $Ý Bà	ˆ×	Ñ	˜8Ó	$€ð  Ð Ø/€à€ðF˜U 3¨ 8™_ó Fð* ˆ"�*‰*ÐDÓ
E€ð+˜s T™zð +¨dó +ð<˜s T™zð <¨c°D©jó <ð&J�có Jð
G˜cð G¨#ð G°#ó Gð  '€ð�3ð ˜3ó ð˜tð ¨ó ð˜ð  ó ð* E€ôJ˜ô JôA�Yô Að ×Ññó  ðð UYò C˜cð C¨ð C¸×9JÑ9JÈTÑ9Qð CÐ]aó Cð: X\ò " ð "¨Cð "¸E×<MÑ<MÐPTÑ<Tð "Ð`dô "r   