Ë
    §‘ˆj„8  ã                   óš  — d Z ddlZddlmZ ddl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  ej$                  e«      Zd	Zd
ededefd„Zdedefd„Z	 	 d&dededededef
d„Zdedefd„Zdededefd„ZdZd'dedededefd„Z ddddœZ!dZ"d
edefd„Z#d
eded ede$fd!„Z%	 	 	 d(deded"ed eded#edefd$„Z&	 	 d)deded eded#edefd%„Z'y)*a<  Read models behind the pairing endpoints.

The pairing job (pairing.py) and the decisions store (pairing_decisions.py) each
own their own table and their own writes; this module only reads, and joins the
two so the merchant-facing surface can show a pair together with its servability
without duplicating that rule.
é    N)Úsql)ÚRealDictCursor)ÚBLOCKING_FIELDS)Úget_db_connection)Úis_servableÚloadÚload_for_anchor)Ú
PAIR_TYPES)ÚnameÚcategoryÚprice_centsÚcurrencyÚ	image_urlÚin_stockÚrowÚprefixÚreturnc           	      ó^   — t         D �ci c]  }|| j                  |› d|› �«      “Œ c}S c c}w )NÚ_)ÚCARD_COLUMNSÚpop)r   r   Úcolumns      ú;/var/www/html/strategist-ai/app/services/pairing/queries.pyÚ_cardr      s/   € Ü@LÖM°fˆF�C—G‘G˜v˜h a¨ xÐ0Ó1Ñ1ÒMÐMùÒMs   ‰*Ú	tenant_idc                 óÂ  — t        «       }	 |j                  t        ¬«      5 }|j                  t	        j
                  d«      j                  t	        j                  | «      «      «       |j                  «       D �cg c]  }t        |«      ‘Œ c}cd d d «       |j                  «        S c c}w # 1 sw Y   nxY w	 |j                  «        y # |j                  «        w xY w)N©Úcursor_factoryzeSELECT category, COUNT(*) AS products FROM {}.strategist_products GROUP BY category ORDER BY category)r   Úcursorr   Úexecuter   ÚSQLÚformatÚ
IdentifierÚfetchallÚdictÚclose)r   ÚconnÚcurÚrs       r   Úlist_categoriesr*      s®   € ÜÓ€Dð	Ø�[‰[¬ˆ[Ó7ð 	5¸3Ø�K‰KœŸ™ð6ó÷ ‰f”S—^‘^ IÓ.Ó/ô	1ð
 &)§\¡\£^Ö4 ”D˜•GÒ4÷	5ð 	5ð 	�
‰
�ùò 5÷	5ð 	5úð 	5ð 	�
‰
�øˆ�
‰
�ús5   ŒC ¢AB.Á;B)ÂB.Â	C Â)B.Â.B7Â3C ÃCr   ÚlimitÚoffsetc                 óÎ  — t        «       }	 |j                  t        ¬«      5 }|rt        j                  d«      nt        j                  d«      }|r|gng }|j                  t        j                  d«      j                  t        j                  | «      |«      |«       |j                  «       d   }|j                  t        j                  d«      j                  t        j                  | «      |«      |||gz   «       |j                  «       D �	cg c]  }	t        |	«      ‘Œ }
}	d d d «       |j                  «        
dœS c c}	w # 1 sw Y   Œ#xY w# |j                  «        w xY w)Nr   zWHERE category = %sÚ z7SELECT COUNT(*) AS total FROM {}.strategist_products {}ÚtotalzœSELECT product_key, name, category, brand, price_cents, currency, in_stock, image_url FROM {}.strategist_products {} ORDER BY product_key LIMIT %s OFFSET %s)Úproductsr/   )r   r   r   r   r!   r    r"   r#   Úfetchoner$   r%   r&   )r   r   r+   r,   r'   r(   ÚwhereÚparamsr/   r)   r0   s              r   Úlist_productsr4   -   s#  € äÓ€DðØ�[‰[¬ˆ[Ó7ð 	9¸3Ù6>”C—G‘GÐ1Ô2ÄCÇGÁGÈBÃKˆEÙ#+�h‘Z°ˆFà�K‰KœŸ™ØIóç‰f”S—^‘^ IÓ.°Ó6¸ô@ð —L‘L“N 7Ñ+ˆEà�K‰KœŸ™ð:ó÷ ‰f”S—^‘^ IÓ.°Ó6¸À%ÈÀÑ8Pô	Rð
 *-¯©«Ö8 Aœ˜Q�Ð8ˆHÐ8÷	9ð  	�
‰
ŒØ ¨5Ñ1Ð1ùò 9÷	9ð 	9ûð  	�
‰
�ús5   ŒE ¢C.EÄEÄ"EÄ$E ÅEÅEÅE ÅE$Úproduct_keyc                 ó¬  — t        «       }	 |j                  t        ¬«      5 }|j                  t	        j
                  d«      j                  t	        j                  | «      «      |f«       |j                  «       }|rt        |«      nd cd d d «       |j                  «        S # 1 sw Y   nxY w	 |j                  «        y # |j                  «        w xY w)Nr   z;SELECT * FROM {}.strategist_products WHERE product_key = %s)r   r   r   r    r   r!   r"   r#   r1   r%   r&   )r   r5   r'   r(   r   s        r   Úget_productr7   E   s¨   € ÜÓ€DðØ�[‰[¬ˆ[Ó7ð 	.¸3Ø�K‰KœŸ™ØMóç‰f”S—^‘^ IÓ.Ó/°+°ôAð —,‘,“.ˆCÙ #”4˜”9¨÷	.ð 	.ð 	�
‰
�÷	.ð 	.úð 	.ð 	�
‰
�øˆ�
‰
�ús#   ŒC ¢A'B#Â		C Â#B,Â(C ÃCc           	      óÞ  — t        «       }	 |j                  t        ¬«      5 }|j                  t	        j
                  d«      j                  t	        j                  | «      t	        j                  | «      «      |f«       |j                  «       D �cg c]  }t        |«      ‘Œ }}ddd«       |j                  «        t        | «      }t        D �ci c]  }|g “Œ }}D ]a  }	t        |	d«      |	d<   |j                  |	d   |	d   |	d   f«      }
t        |	|
«      |	d<   |j!                  |	d   g «      j#                  |	«       Œc |S c c}w # 1 sw Y   Œ¥xY w# |j                  «        w xY wc c}w )	a  Every neighbor of one anchor, grouped by type, each carrying `servable`.

    Servability is recomputed here rather than trusted from the job's report:
    a decision recorded after the last run must be reflected immediately, not
    only after the next re-pair.
    r   a®  
                SELECT n.anchor_key, n.neighbor_key, n.pair_type, n.score,
                       n.confidence, n.source, n.reasons, n.computed_at,
                       p.name AS neighbor_name, p.category AS neighbor_category,
                       p.price_cents AS neighbor_price_cents,
                       p.currency AS neighbor_currency,
                       p.image_url AS neighbor_image_url,
                       p.in_stock AS neighbor_in_stock
                FROM {}.strategist_product_neighbors n
                JOIN {}.strategist_products p ON p.product_key = n.neighbor_key
                WHERE n.anchor_key = %s
                ORDER BY n.score DESC
            NÚneighborÚ
anchor_keyÚneighbor_keyÚ	pair_typeÚservable)r   r   r   r    r   r!   r"   r#   r$   r%   r&   Úload_decisionsr
   r   Úgetr   Ú
setdefaultÚappend)r   r5   r'   r(   r)   ÚrowsÚ	decisionsÚtÚgroupedr   Údecisions              r   Úpairings_forrG   R   sU  € ô Ó€DðØ�[‰[¬ˆ[Ó7ð 	5¸3Ø�K‰KœŸ™ð !ó ÷ ‘œŸ™ yÓ1´3·>±>À)Ó3LÓMØ�ô ð &)§\¡\£^Ö4 ”D˜•GÐ4ˆDÐ4÷	5ð" 	�
‰
Œä˜yÓ)€IÜ(Ö)˜ˆq�"‰uÐ)€GÐ)Øò =ˆÜ  ZÓ0ˆˆJ‰Ø—=‘=Ø�Ñ  NÑ 3°S¸Ñ5EÐFóHˆä% c¨8Ó4ˆˆJ‰Ø×Ñ˜3˜{Ñ+¨RÓ0×7Ñ7¸Õ<ð=ð €Nùò 5÷	5ð 	5ûð" 	�
‰
�üò *s;   ŒE ¢A/E	ÂEÂ#E	Â%E Ã
E*ÅE	Å	EÅE ÅE')ÚsimilarÚ
complementc           	      óÜ  — t        «       }	 |j                  t        ¬«      5 }|j                  t	        j
                  d«      j                  t	        j                  | «      t	        j                  | «      «      |t        t        «      t        t        «      f«       |j                  «       D �cg c]  }t        |«      ‘Œ }}t        | ||«      }ddd«       |j                  «        g }D ]Y  }	|	d   |	d   dœ}
j                  |	d   |	d   |	d	   f«      }t!        |
|«      sŒ8|j#                  |	«       t%        |«      |k\  sŒX |S  |S c c}w # 1 sw Y   Œ€xY w# |j                  «        w xY w)
zÜWhole product rows a shopper may be shown for this anchor.

    Stock and missing_fields are filtered HERE rather than trusted from pairing
    time, because a product can sell out an hour after the graph was built.
    r   a8  
                SELECT p.*, n.pair_type, n.score AS pair_score,
                       n.confidence AS pair_confidence, n.source AS pair_source,
                       n.neighbor_key, n.anchor_key
                FROM {}.strategist_product_neighbors n
                JOIN {}.strategist_products p ON p.product_key = n.neighbor_key
                WHERE n.anchor_key = %s
                  AND n.pair_type = ANY(%s)
                  AND p.in_stock
                  -- Only a blocking gap hides a product from shoppers. A
                  -- missing reference price is not one: it affects
                  -- cross-currency comparison, not whether the card can be
                  -- shown or clicked.
                  AND NOT (p.missing_fields && %s)
                ORDER BY n.score DESC, n.neighbor_key
            NÚpair_confidenceÚpair_source)Ú
confidenceÚsourcer:   r;   r<   )r   r   r   r    r   r!   r"   r#   ÚlistÚSERVED_PAIR_TYPESr   r$   r%   Úload_decisions_for_anchorr&   r?   r   rA   Úlen)r   r5   r+   r'   r(   r)   rB   rC   r=   r   ÚpairrF   s               r   Úservable_neighboursrT      s\  € ô Ó€DðØ�[‰[¬ˆ[Ó7ð 	P¸3Ø�K‰KœŸ™ð !ó ÷ ‘œŸ™ yÓ1´3·>±>À)Ó3LÓMØœdÔ#4Ó5´t¼OÓ7LÐMô!Oð" &)§\¡\£^Ö4 ”D˜•GÐ4ˆDÐ4ô
 2°)¸[È$ÓOˆI÷/	Pð2 	�
‰
Œà€HØò ˆØ!Ð"3Ñ4ÀÀMÑ@RÑSˆØ—=‘=Ø�Ñ  NÑ 3°S¸Ñ5EÐFóHˆä˜4 Ô*ØØ�‰˜ÔÜˆx‹=˜EÓ!ØØ€Oðð €Oùò' 5÷%	Pð 	Pûð2 	�
‰
�ús5   ŒE ¢BEÂ-EÂ?EÃE ÅEÅEÅE ÅE+)çš™™™™™é?g)\�Âõ(ð?)ç333333ã?rU   )ç        rV   )ÚhighÚmediumÚlow)ÚpendingÚapprovedÚrejectedÚautoÚallc                 ó:   — |dk(  ry|dk(  ryt        | d«      rdS dS )z-What a merchant would call this pair's state.r\   r]   Nr^   r[   )r   )r   rF   s     r   Ú	_state_ofra   »   s.   € à�:ÒØØ�:ÒØô !  dÔ+ˆ6Ð:°Ð:ó    ÚstateÚstatusc                 óÖ   — |dk7  r||k7  ry|r=| j                  d«      xs$ | j                  d«      xs i j                  d«      }||k7  ry|rt        |   \  }}|| d   cxk  r|k  sy yy)Nr_   FÚanchor_categoryÚanchorr   ÚscoreT)r?   ÚSCORE_BANDS)r   rc   rd   r   rh   rf   rZ   rX   s           r   Ú_matchesrj   Æ   s†   € Ø�‚˜5 Fš?ØÙð Ÿ7™7Ð#4Ó5ò HØ"Ÿw™w xÓ0Ò6°B×;Ñ;¸JÓGð 	à˜hÒ&ØÙÜ Ñ&‰	ˆˆTØ�c˜'‘lÔ) TÒ)Øð *ØØrb   r:   rh   c           
      óŽ  — |t         vrt        d|› dt         › �«      ‚|r&|t        vrt        d|› dt        t        «      › �«      ‚t	        «       }	 |j                  t        ¬«      5 }|j                  t        j                  d«      j                  t        j                  | «      «      «       |j                  «       d   }|j                  t        j                  d«      j                  t        j                  | «      t        j                  | «      t        j                  | «      «      «       |j                  «       D �	cg c]  }	t        |	«      ‘Œ }
}	ddd«       |j                  «        t!        
«      z
  }t#        | «      }g }|
D ]�  }|j%                  |d	   |d
   |d   f«      }t'        ||«      }|r	|d	   |k7  rŒ7t)        |||||«      sŒG||d<   ||d<   t+        |d«      |d<   t+        |d«      |d<   |j-                  |«       t!        |«      |k\  sŒ� n ||dœS c c}	w # 1 sw Y   ŒÒxY w# |j                  «        w xY w)aM  Pairs, filtered the way a review screen wants them.

    One endpoint rather than one per view: a merchant working through matches
    wants to see what is waiting, then what they approved, then only the strong
    ones in a category, and swapping endpoints for each of those would push the
    same filtering into every client.
    úunknown status: ú; expected one of úunknown score band: r   z=SELECT COUNT(*) AS total FROM {}.strategist_product_neighborsr/   a  
                SELECT n.anchor_key, n.neighbor_key, n.pair_type, n.score,
                       n.confidence, n.source, n.reasons, n.computed_at,
                       a.name AS anchor_name, a.category AS anchor_category,
                       a.price_cents AS anchor_price_cents,
                       a.currency AS anchor_currency,
                       a.image_url AS anchor_image_url,
                       a.in_stock AS anchor_in_stock,
                       p.name AS neighbor_name, p.category AS neighbor_category,
                       p.price_cents AS neighbor_price_cents,
                       p.currency AS neighbor_currency,
                       p.image_url AS neighbor_image_url,
                       p.in_stock AS neighbor_in_stock
                FROM {}.strategist_product_neighbors n
                JOIN {}.strategist_products a ON a.product_key = n.anchor_key
                JOIN {}.strategist_products p ON p.product_key = n.neighbor_key
                ORDER BY n.score DESC
            Nr:   r;   r<   rc   rF   rg   r9   )ÚpairingsÚdropped)ÚDECISION_STATESÚ
ValueErrorri   Útupler   r   r   r    r   r!   r"   r#   r1   r$   r%   r&   rR   r>   r?   ra   rj   r   rA   )r   r+   r:   rd   r   rh   r'   r(   r/   r)   rB   rp   rC   Úmatchedr   rF   rc   s                    r   Úlist_pairingsru   Ø   s(  € ð ”_Ñ$ÜÐ+¨F¨8Ð3EÄoÐEVÐWÓXÐXÙ�œkÑ)ÜÐ/°¨wÐ6HÌÌ{ÓI[ÐH\Ð]Ó^Ð^äÓ€DðØ�[‰[¬ˆ[Ó7ð 	5¸3Ø�K‰KœŸ™ØOóç‰f”S—^‘^ IÓ.Ó/ô1ð —L‘L“N 7Ñ+ˆEà�K‰KœŸ™ð !ó ÷" ‘œŸ™ yÓ1´3·>±>À)Ó3LÜŸ™ yÓ1ó3ô#4ð& &)§\¡\£^Ö4 ”D˜•GÐ4ˆDÐ4÷3	5ð6 	�
‰
Œð ”c˜$“iÑ€Gä˜yÓ)€IØ€GØò ˆØ—=‘=Ø�Ñ  NÑ 3°S¸Ñ5EÐFóHˆä˜#˜xÓ(ˆÙ˜#˜lÑ+¨zÒ9ØÜ˜˜U F¨H°eÔ<ØØˆˆG‰Ø"ˆˆJ‰Ü˜c 8Ó,ˆˆH‰Ü  ZÓ0ˆˆJ‰Ø�‰�sÔÜˆw‹<˜5Ó Ùðð   ¨GÑ4Ð4ùò5 5÷3	5ð 	5ûð6 	�
‰
�ús7   ÁH2 Á'CH&ÅH!ÅH&ÅH2 È!H&È&H/È+H2 È2Ic                 óZ  — |t         vrt        d|› dt         › �«      ‚|r&|t        vrt        d|› dt        t        «      › �«      ‚t	        | dd¬«      d   }i }|D ]„  }|j                  |d   |d   |d	   d
d
d
d
d
ddg dœ
«      }||d   xx   dz  cc<   t        ||d   |||«      sŒL|dxx   dz  cc<   t        |d   |d   «      |d<   |d   j                  |d   «       Œ† g }	|j                  «       D ]N  }|d   sŒ	|j                  d«      }
t        t        |
«      t        |
«      z  d«      |d<   |	j                  |«       ŒP |	j                  d„ ¬«       |	d| }|D ��cg c]	  }|d   }|‘Œ }}}|t        |	«      |r#t        t        |«      t        |«      z  d«      dœS ddœS c c}}w )a}  Products with matches, and how many of each state they hold.

    A merchant thinks in products, not in pairs: "this dress has 4 to review"
    is reviewable, a flat list of 229 pairs from 80 different products is not.

    Every anchor carries the full breakdown, not just the filtered count, so a
    screen can show "4 pending, 2 approved" without asking again per product.
    rl   rm   rn   i'  r_   )r+   rd   ro   r:   rg   r   rW   )
r:   rg   Úmatchingr[   r\   r]   r^   Ú	top_scoreÚ	avg_scoreÚ_scoresrc   é   rw   rx   rh   rz   é   ry   c                 ó   — | d    | d   fS )Nrx   r:   © )Úas    r   ú<lambda>zlist_anchors.<locals>.<lambda>N  s   €   ;¡ °°<±ÐA€ rb   )ÚkeyN)Úanchorsr/   ry   )rq   rr   ri   rs   ru   r@   rj   ÚmaxrA   Úvaluesr   ÚroundÚsumrR   Úsort)r   r+   rd   r   rh   Ú
everythingrE   r   Úentryr‚   ÚscoresÚshownr   ÚsÚ
all_scoress                  r   Úlist_anchorsrŽ     sý  € ð ”_Ñ$ÜÐ+¨F¨8Ð3EÄoÐEVÐWÓXÐXÙ�œkÑ)ÜÐ/°¨wÐ6HÌÌ{ÓI[ÐH\Ð]Ó^Ð^ä˜y°¸uÔEÀjÑQ€Jà€GØò 2ˆØ×"Ñ" 3 |Ñ#4Ø˜lÑ+Ø˜(‘mØØ a°QÀØØØñ7
ó ˆð 	ˆc�'‰lÓ˜qÑ ÓÜ�C˜˜W™ v¨x¸Õ?Ø�*Ó Ñ"ÓÜ!$ U¨;Ñ%7¸¸W¹Ó!FˆE�+ÑØ�)Ñ×#Ñ# C¨¡LÕ1ð2ð  €GØ—‘Ó!ò ˆØ�ZÒ ØØ—‘˜9Ó%ˆä"¤3 v£;´°V³Ñ#<¸aÓ@ˆˆkÑØ�‰�uÕðð ‡L�LÑA€LÔBØ�F�UˆO€EØ"×=˜¨Q¨{©^ q’!Ð=€JÑ=àÜ�W“ÙDN”Uœ3˜z›?¬S°«_Ñ<¸aÓ@ñð ð UXñð ùó >s   Å"F')Né2   r   )é   )éd   Nr[   NN)r�   r[   NN)(Ú__doc__ÚloggingÚpsycopg2r   Úpsycopg2.extrasr   Úapp.services.catalog.normaliser   Úapp.services.infra.databaser   Úapp.services.pairing.decisionsr   r   r>   r	   rQ   Úapp.services.pairing.rulesr
   Ú	getLoggerÚ__name__Úloggerr   r%   Ústrr   rO   r*   Úintr4   r7   rG   rP   rT   ri   rq   ra   Úboolrj   ru   rŽ   r~   rb   r   ú<module>r       sÚ  ðñó å Ý *å :Ý 9÷ñ õ 2à	ˆ×	Ñ	˜8Ó	$€ð€ðNˆtð N˜Sð N Tó Nð˜sð  tó ð FHØ!"ñ2˜Sð 2¨Cð 2¸sð 2Øð2Ø'+ó2ð0
˜3ð 
¨Só 
ð$˜Cð $¨cð $°dó $ðT .Ð ñ- 3ð -°Sð -Àð -ÈTó -ðf ØØñ€ð E€ð;�4ð ; có ;ð�$ð ˜sð ¨Cð ÀTó ð$ GKØ;?Ø#ñD5˜Sð D5¨ð D5Àð D5ØðD5Ø58ðD5àðD5à(,óD5ðN AJØ48ñ6˜Cð 6¨ð 6¸#ð 6Øð6Ø.1ð6Ø=Aô6rb   