Ë
    çÎj)•  ã                   óÖ	  — U 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ZddlmZ ddl	m
Z
 ddlZddlmZmZmZmZ ddlmZ ddlmZ ddlmZmZ dd	l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%m&Z& ddl'm(Z(m)Z)m*Z* ddl+m,Z, ddl-m.Z. ddl/m0Z1 ddl2m3Z3m4Z4 ddl5m6Z6 ddl7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?  e.«         ej€                  d«      ZA ej„                  dd«      ZC ejˆ                  d«      ZEdzd„ZFdzd„ZGedefd„«       ZH eddeHe$rdndde$rdnd¬ «      ZIddlJZKddlLZMddlNZNeIjŸ                  eKj                   «       eIjŸ                  eMj                   «       eIjŸ                  eNj                   «       eIj£                  ee#d!g d"¢g d#¢¬$«        G d%„ d&«      ZReIj£                  eR«       eIj§                  d'«      d(efd)„«       ZT G d*„ d+e«      ZUeIj­                  d,d-¬.«      d/eUd(efd0„«       ZWeIj±                  eY«      d(ed1eYfd2„«       ZZ G d3„ d4e«      Z[ G d5„ d6e«      Z\ G d7„ d8e«      Z]d9e^de^fd:„Z_d9e^fd;„Z`d<e^deaeb   fd=„Zcd>eaeb   deaeb   fd?„ZdeIjË                  d@«      dA„ «       ZfeIjË                  dB«       edCdDdE¬F«      fd(ed9e^fdG„«       ZgeIjË                  dH«       edC«       edC«       edIdJ¬K«      fd9e^dLe^dMehdNe^dOe^f
dP„«       ZieIjË                  dQ«       edC«       edC«      fd<e^dRe^d9e^dLe^dMehdNe^fdS„«       ZjeIj­                  dT«      d/e]d(efdU„«       ZkeIjË                  dV«       edCdDdE¬F«      fd9e^fdW„«       ZleIj­                  dX«      d/e\d(efdY„«       Zm G dZ„ d[e«      Znd\ eo«       d]œZpebeqd^<   deofd_„ZreIj­                  d`«      d/end(efda„«       ZseIjË                  db«      d9e^d(efdc„«       ZteIj­                  dd«      d(efde„«       Zud<e^d9e^dfeaeb   deaeve^ewf      fdg„Zxdhe^die
eh   ddfdj„Zyddkl7mzZz  ezjö                  ey«       dhe^ddfdl„Z|eIjË                  dX«       edCdDdE¬F«      fd(ed9e^fdm„«       Z}eIjË                  dn«       edC«       edC«      fdoe^dLe^d(edMehdNe^f
dp„«       Z~eIjË                  dq«       edC«       edC«      fdoe^d(edMehdNe^fdr„«       Ze€dsk(  rRddlZddl�Z� ej„                  dtd«      Z‚ eh e�j                  dudv«      «      Z„ e��j
                  dwe"e!e‚e‚rdxne„¬y«       yy){u³  
Meeconic Portal Backend â€” API REST du portail B2C des familles (multi-Ã©cole).

Routes portail :
  GET  /api/health
  GET  /api/portal/photos?code=XXX          â†’ galerie (aperÃ§us filigranÃ©s signÃ©s)
  POST /api/portal/consent
  POST /api/portal/orders                   â†’ commande + PaymentIntent Stripe
  GET  /api/portal/orders?code=XXX          â†’ historique + liens de tÃ©lÃ©chargement (commandes payÃ©es)
  GET  /api/portal/download/{order}/{file}  â†’ original HD, lien signÃ©, commande payÃ©e uniquement
  GET  /api/media/preview/{code}/{file}     â†’ aperÃ§u filigranÃ©, lien signÃ©
  POST /api/stripe/webhook

Les originaux ne sont JAMAIS servis comme fichiers statiques.
é    N)Úasynccontextmanager)ÚOptional)ÚFastAPIÚHTTPExceptionÚQueryÚRequest)ÚCORSMiddleware)ÚGZipMiddleware)ÚFileResponseÚJSONResponse)Ú	BaseModelÚField)Úconfig)	Ú
PHOTOS_DIRÚ
THUMBS_DIRÚINCOMING_DIRÚPORTÚHOSTÚCORS_ORIGINSÚIS_PRODUCTIONÚSTRIPE_SECRET_KEYÚSTRIPE_WEBHOOK_SECRET)Úget_dbÚinit_core_tablesÚTABLE_OPTIONS)Úrun_as_leader)Úsetup_logging)Úenforce)Ú	client_ipÚvalidate_production_config)Ú
utcnow_iso)Úaudit_serviceÚalert_serviceÚmedia_serviceÚphoto_serviceÚphotographer_serviceÚprotection_serviceÚscan_serviceÚschool_serviceÚmeeconicÚEMAIL_ENABLEDÚfalsez^[^@\s]+@[^@\s]+\.[^@\s]+$Úreturnc                  ó  — t        «       5 } | j                  dt        › �«       | j                  «        d d d «       t        j                  dt        j                  t        j                  t        j                  «       y # 1 sw Y   ŒLxY w)NaÝ  
        CREATE TABLE IF NOT EXISTS orders (
            id                       VARCHAR(36)  NOT NULL PRIMARY KEY,
            family_code              VARCHAR(32)  NOT NULL,
            customer_name            VARCHAR(200),
            customer_email           VARCHAR(200),
            shipping_address         TEXT,
            total                    DOUBLE NOT NULL,
            status                   VARCHAR(20)  NOT NULL DEFAULT 'pending',
            items_json               MEDIUMTEXT NOT NULL,
            created_at               VARCHAR(40)  NOT NULL,
            payment_status           VARCHAR(20)  NOT NULL DEFAULT 'unpaid',
            stripe_payment_intent_id VARCHAR(80),
            school_id                VARCHAR(64),
            batch_status             VARCHAR(20)  NOT NULL DEFAULT 'to_print',
            KEY idx_orders_code (family_code),
            KEY idx_orders_school (school_id),
            KEY idx_orders_pi (stripe_payment_intent_id)
        ) zDatabase ready at %s:%s/%s)
r   Úexecuter   ÚcommitÚlogÚinfor   Ú
MYSQL_HOSTÚ
MYSQL_PORTÚMYSQL_DB)Úconns    ú/var/www/photos/backend/main.pyÚinit_dbr8   9   sn   € Ü	‹ð �TØ�‰ð ô$ ˆ/ð%ô 	ð& 	�‰Œ÷)ô* ‡H�HÐ)¬6×+<Ñ+<¼f×>OÑ>OÔQW×Q`ÑQ`Õa÷+ð ús   ‹)A?Á?Bc                  óü   — ddl } t        t        t        fD ]  }|j	                  dd¬«       Œ t        «        t        «         | j                  «        t        j                  «        ddl
m} |j                  «        y)u   CrÃ©e dossiers et tables, migre les anciennes installations. Idempotent.
    AppelÃ© au dÃ©marrage (lifespan) et par les tests.r   NT)ÚparentsÚexist_ok)Úshoot_service)Úadminr   r   r   Úmkdirr   r8   Úinit_admin_dbr&   Úinit_photographer_tableÚservicesr<   Úinit_shoot_tables)Úadmin_moduleÚdr<   s      r7   Úinit_allrE   R   sa   € ó !Üœ*¤lÐ3ò -ˆØ	�‰˜ tˆÕ,ð-äÔÜ„IØ€L×ÑÔ Ü×0Ñ0Ô2Ý&Ø×#Ñ#Õ%ó    Úappc                ó>  K  — t        «       }|r@t        rt        ddj                  |«      z   «      ‚|D ]  }t        j                  d|«       Œ t        «        g }t        j                  rèddl	m
} ddlm} ddlm} |j                  t!        j"                  t%        d|«      d	¬
«      «       |j                  t!        j"                  t%        d|«      d¬
«      «       |j                  t!        j"                  t%        d|«      d¬
«      «       t        j&                  r6ddlm} |j                  t!        j"                  t%        d|«      d¬
«      «       t        j-                  dt        j.                  t0        «       	 d ­–— |D ]  }|j3                  «        Œ t!        j4                  |ddiŽƒ d {  –—†  t        j-                  d«       y 7 Œ# |D ]  }|j3                  «        Œ t!        j4                  |ddiŽƒ d {  –—†7   t        j-                  d«       w xY w­w)Nz*Configuration de production invalide :
 - z
 - z*Config (non bloquant hors production) : %sr   )Úrun_cleanup_loop)Úrun_backup_loop)Úrun_monitor_loopÚcleanupzleader-cleanup)ÚnameÚbackupzleader-backupÚmonitorzleader-monitor)Úrun_all_organizersÚ	organizerzleader-organizerz+Meeconic Portal API ready (env=%s, port=%d)Úreturn_exceptionsTz#Meeconic Portal API stopped cleanly)r    r   ÚRuntimeErrorÚjoinr1   ÚwarningrE   r   ÚBACKGROUND_TASKS_ENABLEDÚservices.cleanup_servicerI   Úservices.backup_servicerJ   Úservices.monitor_servicerK   ÚappendÚasyncioÚcreate_taskr   ÚORGANIZER_ENABLEDÚservices.organizer_servicerP   r2   ÚAPP_ENVr   ÚcancelÚgather)	rG   ÚproblemsÚpÚtasksrI   rJ   rK   rP   Úts	            r7   Úlifespanrf   b   s›  è ø€ ä)Ó+€HÙÝÜÐLÈwÏ|É|Ð\dÓOeÑeÓfÐfØò 	IˆAÜ�K‰KÐDÀaÕHð	Iä„Jà "€EÜ×&Ò&Ý=Ý;Ý=Ø�‰”W×(Ñ(¬°yÐBRÓ)SÐZjÔkÔlØ�‰”W×(Ñ(¬°xÀÓ)QÐXgÔhÔiØ�‰”W×(Ñ(¬°yÐBRÓ)SÐZjÔkÔlÜ×#Ò#ÝEØ�L‰Lœ×,Ñ,¬]¸;ÐHZÓ-[ÐbtÔuÔvÜ‡H�HÐ:¼F¿N¹NÌDÔQð8Üàò 	ˆAØ�H‰H�Jð	ä�n‰n˜eÐ<°tÑ<×<Ð<Ü�‰Ð6Õ7ð 	=ùð ò 	ˆAØ�H‰H�Jð	ä�n‰n˜eÐ<°tÑ<×<Ñ<Ü�‰Ð6Õ7üs6   ‚E:HÅ=G Æ/HÆ1GÆ2HÇ0HÇ>HÇ?HÈHzMeeconic Portal APIú2.0.0z/docsz/openapi.json)ÚtitleÚversionrf   Údocs_urlÚ	redoc_urlÚopenapi_urlT)ÚGETÚPOSTÚPATCHÚPUTÚDELETEÚOPTIONS)ÚAuthorizationzContent-TypezX-School)Úallow_originsÚallow_credentialsÚallow_methodsÚallow_headersc                   ó    — e Zd ZdZdZd„ Zd„ Zy)Ú_SelectiveGZipzbGZip for JSON/SVG/text only: photos and zips are already compressed, gzipping them just burns CPU.)z/api/media/z/api/portal/downloadz/api/admin/cards.zipz/api/admin/exportc                 ó6   — || _         t        |d¬«      | _        y )Né   )Úminimum_size)rG   r
   Úgzip)ÚselfrG   s     r7   Ú__init__z_SelectiveGZip.__init__™   s   € ØˆŒÜ" 3°TÔ:ˆ�	rF   c              ƒ   óÎ   K  — |d   dk(  r:|d   j                  | j                  «      s| j                  |||«      ƒ d {  –—†  y | j                  |||«      ƒ d {  –—†  y 7 Œ!7 Œ­w)NÚtypeÚhttpÚpath)Ú
startswithÚ_SKIPr}   rG   )r~   ÚscopeÚreceiveÚsends       r7   Ú__call__z_SelectiveGZip.__call__�   s\   è ø€ Ø�‰=˜FÒ"¨5°©=×+CÑ+CÀDÇJÁJÔ+OØ—)‘)˜E 7¨DÓ1×1Ñ1à—(‘(˜5 '¨4Ó0×0Ñ0ð 2øà0ús"   ‚=A%¿A!Á A%ÁA#ÁA%Á#A%N)Ú__name__Ú
__module__Ú__qualname__Ú__doc__r…   r   r‰   © rF   r7   ry   ry   ”   s   „ Ùlà`€Eò;ó1rF   ry   r‚   Úrequestc              ƒ   óœ  K  —  || «      ƒ d {  –—† }|j                   }|j                  dd«       |j                  dd«       |j                  dd«       |j                  dd«       |j                  d	d«       t        r|j                  d
d«       | j                  j                  j                  d«      r|j                  dd«       |S 7 Œ»­w)NzX-Content-Type-OptionsÚnosniffzReferrer-Policyzsame-originzX-Frame-OptionsÚDENYzPermissions-Policyz<camera=(self), microphone=(), geolocation=(), payment=(self)zCross-Origin-Opener-PolicyzStrict-Transport-Securityz#max-age=31536000; includeSubDomainsz/api/úCache-Controlzno-store)ÚheadersÚ
setdefaultr   Úurlrƒ   r„   )r�   Ú	call_nextÚresponseÚhs       r7   Úsecurity_headersrš   §   s­   è ø€ á˜wÓ'×'€HØ×Ñ€AØ‡L�LÐ)¨9Ô5Ø‡L�LÐ" MÔ2Ø‡L�LÐ" FÔ+à‡L�LÐ%Ð'eÔfØ‡L�LÐ-¨}Ô=ÝØ	�‰Ð0Ð2WÔXØ‡{�{×Ñ×"Ñ" 7Ô+Ø	�‰�_ jÔ1Ø€Oð (ús   ‚CŽC
�B<Cc                   óh   — e Zd ZU  edd¬«      Zeed<    edd¬«      Zeed<    edd¬«      Zeed<   y	)
ÚClientErrorÚ éô  )Ú
max_lengthÚmessageiÜ  ÚstackéÈ   r–   N)	rŠ   r‹   rŒ   r   r    ÚstrÚ__annotations__r¡   r–   rŽ   rF   r7   rœ   rœ   ¸   s5   … Ù˜¨Ô,€GˆSÓ,Ù�r dÔ+€Eˆ3Ó+Ù�R CÔ(€CˆÔ(rF   rœ   z/api/client-erroréÌ   )Ústatus_codeÚbodyc              ƒ   óÚ   K  — t        dt        |«      › �dd«       d„ }t        j                  d || j                  «       || j
                  «       || j                  «      dd «       y­w)zhBrowser crash report (ErrorBoundary beacon). Rate limited, logged only: never trust or echo the content.zclient-error:é   é  c                 ó@   — dj                  | j                  «       «      S )Nú )rT   Úsplit)Úvs    r7   ú<lambda>zclient_error.<locals>.<lambda>Â   s   € �S—X‘X˜aŸg™g›iÓ(€ rF   zBrowser error on %s: %s | %sNé,  )Úrate_limit_enforcer   r1   rU   r–   r    r¡   )r§   r�   Úflats      r7   Úclient_errorr³   ¾   s`   è ø€ ô ˜¤y°Ó'9Ð&:Ð;¸RÀÔFÙ(€DÜ‡K�KÐ.±°T·X±X³ÁÀTÇ\Á\Ó@RÑTXÐY]×YcÑYcÓTdÐeiÐfiÐTjÕkùó   ‚A)A+Úexcc              ƒ   óf  K  — t         j                  d| j                  | j                  j                  «       t        j                  d| j                  j                  › �d| j                  › d| j                  j                  › �t        |«      j                  › d|› �«       t        dddi¬	«      S ­w)
NzUnhandled error on %s %sz	http-500:zErreur 500 sur r¬   ú: rž   ÚdetailzErreur interne du serveur.©r¦   Úcontent)
r1   Ú	exceptionÚmethodr–   rƒ   r#   Únotifyr�   rŠ   r   )r�   rµ   s     r7   Úunhandled_exceptionr¾   Æ   s–   è ø€ ä‡M�MÐ,¨g¯n©n¸g¿k¹k×>NÑ>NÔOÜ×Ñ˜9 W§[¡[×%5Ñ%5Ð$6Ð7¸?È7Ï>É>ÐJZÐZ[Ð\c×\gÑ\g×\lÑ\lÐ[mÐ9nÜ  ›I×.Ñ.Ð/¨r°#°Ð7ô9ä C°(Ð<XÐ1YÔZÐZùs   ‚B/B1c                   óÂ   — e Zd ZU  edd¬«      Zeed<    edd¬«      Zeed<    edd¬	«      Zeed
<    edd¬«      Z	e
ed<   eed<   dZeed<    edd¬	«      Zee   ed<   y)Ú	OrderItemé   r¢   ©Ú
min_lengthrŸ   Úphoto_filenameé(   Ú	format_idr�   éx   ©ÚdefaultrŸ   Úformat_labelr©   )ÚgeÚleÚquantityÚ
unit_priceFÚis_magicNÚ	job_label)rŠ   r‹   rŒ   r   rÄ   r£   r¤   rÆ   rÊ   rÍ   ÚintÚfloatrÏ   ÚboolrÐ   r   rŽ   rF   r7   rÀ   rÀ   Ð   sk   … Ù ¨A¸#Ô>€N�SÓ>Ù ¨A¸"Ô=€I�SÓ=Ù ¨¸Ô<€L�SÓ<Ù  A¨"Ô-€H�SÓ-ØÓØ!€H�TÓ!Ù%*°4ÀCÔ%H€I�X˜c‘]ÔHrF   rÀ   c                   ó¦   — e Zd ZU eed<    edd¬«      Zeed<    edd¬«      Zeed<    ed	d
¬«      Ze	e   ed<    edd¬«      Z
ee   ed<   eed<   y	)ÚCreateOrderRequestÚfamily_coderÁ   rÅ   rÂ   Ú
class_nameé   r¢   Úcustomer_emailNrž   rÈ   Úshipping_addressé2   ÚitemsÚtotal)rŠ   r‹   rŒ   r£   r¤   r   r×   rÙ   rÚ   r   rÜ   ÚlistrÀ   rÒ   rŽ   rF   r7   rÕ   rÕ   Ú   s]   … ØÓñ "¨Q¸2Ô>€J�cÓ>Ù!¨Q¸3Ô?€N�cÓ?Ù&+°DÀSÔ&IÐ�h˜s‘mÓIÙ(-¸ÀrÔ(J€E�d˜9‘oÓJØÔrF   rÕ   c                   ó   — e Zd ZU eed<   y)ÚConsentRequestrÖ   N)rŠ   r‹   rŒ   r£   r¤   rŽ   rF   r7   rà   rà   å   s   … ØÔrF   rà   Úcodec                 ód   — dj                  | xs dj                  «       «      j                  «       S )u[   MayÃºsculas y sin espacios: lo que la familia teclea ('tst 1001') no siempre es lo impreso.r�   )rT   r­   Úupper)rá   s    r7   Ú
_norm_coderä   ë   s'   € à�7‰7�D’J˜B×%Ñ%Ó'Ó(×.Ñ.Ó0Ð0rF   c                 ó&  — t        j                  | «      st        dd¬«      ‚t        «       5 }|j	                  d| t        j                  | «      | f«      j                  «       }ddd«       r
|d   r|d   st        dd¬«      ‚|S # 1 sw Y   Œ$xY w)u9   Famille connue en base + son Ã©cole (ou 404 gÃ©nÃ©rique).é”  u+   Code introuvable â€” vÃ©rifiez votre carte.©r¸   z«SELECT f.*, s.active AS school_active FROM families f LEFT JOIN schools s ON s.id = f.school_id WHERE f.code=? OR REPLACE(f.code,'-','')=? ORDER BY (f.code=?) DESC LIMIT 1NÚ	school_idÚschool_active)r$   Ú
valid_coder   r   r/   r%   Ú	norm_codeÚfetchone)rá   r6   Úfams      r7   Ú_get_familyrî   ð   sœ   € ä×#Ñ# DÔ)Ü˜CÐ(UÔVÐVä	‹ð D�TØ�l‰lðZð ”=×*Ñ*¨4Ó0°$Ð7ó	9÷ :B¹»ð	 	÷Dñ �c˜+Ò&¨c°/Ò.BÜ˜CÐ(UÔVÐVØ€J÷Dð Dús   ­7BÂBrè   c                 óH  — t        «       5 }|j                  d| f«      j                  «       }ddd«       D �cg c]R  }|d   d|d   › �|d   |d   xs |d   |d   xs |d   |d   t        |d	   «      |d
   rd| › d|d   › d|d
   › �nddœ‘ŒT c}S # 1 sw Y   ŒgxY wc c}w )z`Catalogue items the family can add next to prints and digital files (with their example visual).zTSELECT * FROM school_products WHERE school_id=? AND active=1 ORDER BY sort_order, idNÚidúproduct-Úname_frÚname_enÚname_esÚpriceÚphysicalÚ	has_imagez/api/portal/product-image/ú/z.webp?v=)rð   rÆ   rò   ró   rô   rõ   rö   Ú	image_url)r   r/   ÚfetchallrÓ   )rè   r6   ÚrowsÚrs       r7   Ú_active_productsrý      sÚ   € ä	‹ð 5�TØ�|‰|ÐrØ&˜Ló*ß*2©(«*ð 	÷5ð öð
 ð	 �‰g h¨q°©w¨iÐ$8ÀQÀyÁ\Ø�Y‘<Ò/ 1 Y¡<¸A¸i¹LÒ<XÈAÈiÉLØ�7‘¬¨a°
©mÓ)<ØcdÐepÒcqÐ1°)°¸A¸aÀ¹g¸YÀhÈqÐQ\É~ÐN^Ñ_Ðw{ó	ò ð ÷5ð 5üòs   ‹"B¹ABÂBrÜ   c                 óÊ   — t        «       g }}| D ]Q  }|j                  d«      t        j                  v sŒ%|d   |vsŒ-|j	                  |d   «       |j                  |«       ŒS |S )u<   Fichiers originaux auxquels une commande payÃ©e donne droit.rÆ   rÄ   )ÚsetÚgetr)   ÚDIGITAL_FORMATSÚaddrZ   )rÜ   ÚseenÚoutÚits       r7   Úentitled_downloadsr    se   € ä“�rˆ#€DØò ˆØ�6‰6�+Ó¤.×"@Ñ"@Ò@ÀRÐHXÑEYÐaeÒEeØ�H‰H�RÐ(Ñ)Ô*Ø�J‰J�r�Nðð €JrF   z/api/healthc               ƒ   óà   K  — d} 	 t        «       5 }|j                  d«      j                  «        d d d «       t	        | rdnd| rdnd| dd	œ¬
«      S # 1 sw Y   Œ"xY w# t        $ r d} Y Œ2w xY w­w)NTzSELECT 1Fr¢   é÷  ÚokÚdegradedrg   )ÚstatusÚdbri   r¹   )r   r/   rì   Ú	Exceptionr   )Údb_okr6   s     r7   Úhealthr    sz   è ø€ à€EðÜ‹Xð 	0˜Ø�L‰L˜Ó$×-Ñ-Ô/÷	0ô ©5¡C°cÙ38©4¸jÐPUÐbiÑ jôlð l÷		0ð 	0ûäò ØŠðüs=   ‚A.†
A � A°A ¸A.ÁAÁA ÁA+Á(A.Á*A+Á+A.z/api/portal/photos.é   é   rÂ   c              ƒ   ó´  K  — t        |«      }t        | «      }ddlm}m} d|› �} ||dd¬«      r)t        j                  d|ddddœ¬	«       t        d
d¬«      ‚	 t        |«      }|d   }|d   }t        j                  |«      }t        |d   «      }	|	rt        j                  ||«      ng }
t        d„ |
D «       «      }t        |d   «      }||d   |
|	xr |
 t        j                   |«      t#        |«      |	|d   |d   dk(  xr |dkD  t%        d||z
  «      t'        j(                  t'        j*                  |«      «      dœS # t        $ r
  ||«       ‚ w xY w­w)Nr   )Ú
is_blockedÚ
record_hitzphotos-lookup:é   iX  ©Úmax_attemptsÚwindow_secondszauth.code_bruteforce_blockedÚcritical)Ú
ip_addressÚseverityÚdetailsi­  u&   Too many attempts â€” try again later.rç   rá   rè   Ú
consent_atc              3   óJ   K  — | ]  }|d    j                  d«      sŒd–— Œ y­w)ÚfilenameÚia_rÁ   N)r„   )Ú.0rc   s     r7   ú	<genexpr>zget_photos.<locals>.<genexpr><  s!   è ø€ ÒG˜ q¨¡}×'?Ñ'?ÀÕ'F”!ÑGùs   ‚#œ#Úai_max_per_familyÚschool_nameÚ	rgpd_daysÚ
ai_enabledÚtrue)rÖ   r$  ÚphotosÚphotos_pendingÚpricingÚproductsÚconsent_givenr%  r&  Úai_remainingÚ
protection)rä   r   Úcore.rate_limitr  r  r"   Ú	log_eventr   rî   r)   Úget_settingsrÓ   r$   Úlist_family_photosÚsumrÑ   Úpricing_from_settingsrý   Úmaxr'   Úpublic_viewÚget_protection)r�   rá   Úipr  r  Úkeyrí   rè   Úsettingsr,  r(  Úai_usedÚai_maxs                r7   Ú
get_photosr=  %  s}  è ø€ ä�dÓ€DÜ	�7Ó	€B÷ 7Ø˜2˜$Ð
€CÙ�# B°sÕ;Ü×ÑÐ >È2ÐXbØ9;ÈsÑ(Sõ	Uä˜CÐ(PÔQÐQðÜ˜$Óˆð
 ˆv‰;€DØ�KÑ €IÜ×*Ñ*¨9Ó5€HÜ˜˜\Ñ*Ó+€MÙBOŒ]×-Ñ-¨i¸Ô>ÐUW€FÜÑG˜VÔGÓG€GÜ�Ð-Ñ.Ó/€FàØ" =Ñ1Ø Ø'Ò6°¨JÜ(×>Ñ>¸xÓHÜ*¨9Ó5Ø'Ø" ;Ñ/Ø" <Ñ0°FÑ:ÒI¸vÈ¹zÜ˜a ¨'Ñ!1Ó2ô -×8Ñ8Ô9K×9ZÑ9ZÐ[dÓ9eÓfñð øô ò Ù�3ŒØðüs   ‚AEÁE Á&CEÅEÅEz$/api/media/preview/{code}/{filename}Úfullz^(full|thumb)$)Úpatternr  ÚexpÚsigÚsizec              ƒ   ó:  K  — t        j                  | |||«      st        dd¬«      ‚t        j                  t
        j                  | «      ƒ d {  –—† }|r t        j                  || ||«      ƒ d {  –—† nd }|€t        dd¬«      ‚t        |dddi¬	«      S 7 ŒG7 Œ'­w)
Né“  õ   Lien expirÃ© ou invalide.rç   ræ   zPhoto introuvable.ú
image/webpr“   zprivate, max-age=3600©Ú
media_typer”   )	r$   Úcheck_preview_signaturer   r[   Ú	to_threadr)   Úschool_id_for_familyÚrender_previewr   )rá   r  r@  rA  rB  rè   rƒ   s          r7   Úmedia_previewrM  O  sŸ   è ø€ ô ×0Ñ0°°xÀÀcÔJÜ˜CÐ(CÔDÐDÜ×'Ñ'¬×(KÑ(KÈTÓR×R€IÙR[”×-Ñ-¨i¸¸xÈÓN×NÐNÐae€DØ€|Ü˜CÐ(<Ô=Ð=Ü˜¨Ø!0Ð2IÐ JôLð Lð	 SøØNús$   ‚ABÁBÁ!BÁ1BÁ2&BÂBzC/api/media/admin-thumb/{school_id}/{photographer}/{code}/{filename}Úphotographerc           	   ƒ   ó®   K  — t        j                  t        j                  | |||||«      ƒ d {  –—† }|€t	        dd¬«      ‚t        |dddi¬«      S 7 Œ#­w)NrD  rE  rç   rF  r“   zprivate, max-age=600rG  )r[   rJ  r$   Úadmin_thumb_pathr   r   )rè   rN  rá   r  r@  rA  rƒ   s          r7   Úmedia_admin_thumbrQ  \  s_   è ø€ ô ×"Ñ"¤=×#AÑ#AÀ9ÈlÐ\`ÐbjÐloÐqtÓu×u€DØ€|Ü˜CÐ(CÔDÐDÜ˜¨ÀÐQgÐ?hÔiÐið vús   ‚-A¯A°$Az/api/portal/consentc           	   ƒ   ó�  K  — t        t        | j                  «      «      }|d   }t        |«      }t	        «       }t        «       5 }|j                  d|||f«       |j                  d|f«      j                  «       }|j                  «        ddd«       t        j                  d|d|d|d   ¬	«       d
d   dœS # 1 sw Y   Œ/xY w­w)z]Enregistre le consentement RGPD explicite. Idempotent : le premier
    consentement fait foi.rá   zkUPDATE families SET consent_at = COALESCE(consent_at, ?), consent_ip = COALESCE(consent_ip, ?) WHERE code=?z,SELECT consent_at FROM families WHERE code=?Nzconsent.givenÚfamilyr2   rè   )r  Úresource_typeÚresource_idr  rè   Tr  )r,  r  )rî   rä   rÖ   r   r!   r   r/   rì   r0   r"   r0  )r§   r�   rí   rá   r8  Únowr6   Úrows           r7   Úgive_consentrX  e  sÅ   è ø€ ô ”j ×!1Ñ!1Ó2Ó
3€CØˆv‰;€DÜ	�7Ó	€BÜ
‹,€CÜ	‹ð �TØ�‰ØyØ�"�dˆOô	ð �l‰lÐIÈDÈ7ÓS×\Ñ\Ó^ˆØ�‰Œ÷ô ×Ñ˜O¸È(Ð`dØ%+°s¸;Ñ7GõIà!°°\Ñ1BÑCÐC÷ð üs   ‚ACÁAB:Â.CÂ:CÂ?Cz/api/portal/classesc              ƒ   óL  K  — t        t        | «      «      }t        «       5 }|j                  d|d   f«      j	                  «       }|j                  d|d   f«      D �cg c]  }|d   ‘Œ	 }}ddd«       ddlm} t        d	„ D «       |¬
«      }|dœS c c}w # 1 sw Y   Œ-xY w­w)zZClasses of the family's school, for the checkout dropdown (typed once in the back-office).z1SELECT name FROM school_classes WHERE school_id=?rè   z?SELECT class_name FROM students WHERE family_code=? ORDER BY idrá   r×   Nr   )Úclass_sort_keyc              3   ó&   K  — | ]	  }|d    –— Œ y­w)rM   NrŽ   )r!  rü   s     r7   r"  zget_classes.<locals>.<genexpr>�  s   è ø€ Ò, !�A�f•IÑ,ùs   ‚)r9  )ÚclassesÚfamily_classes)rî   rä   r   r/   rú   r=   rZ  Úsorted)rá   rí   r6   rû   rü   ÚminerZ  Únamess           r7   Úget_classesra  x  sº   è ø€ ô ”j Ó&Ó
'€CÜ	‹ð `�TØ�|‰|ÐOÐRUÐVaÑRbÐQdÓe×nÑnÓpˆØ)-¯©ØMÐPSÐTZÑP[È~ó*_ö ` A��,“ð `ˆð `÷`õ %ÜÑ, tÔ,°.ÔA€EØ°Ñ5Ð5ùò	`÷`ð `üs.   ‚B$¡=BÁBÁ*BÁ,'B$ÂBÂB!ÂB$z/api/portal/ordersc              ƒ   ób  K  — t        | j                  «      }t        |«      }t        d|› �dd¬«       t	        |«      }|d   }|d   st        dd¬	«      ‚t        j                  |d
   «      }|d   dk7  rt        dd¬	«      ‚| j                  j                  «       }t        j                  |«      st        dd¬	«      ‚dj                  | j                  j                  «       «      }t        «       5 }|j!                  d|d
   |f«      j#                  «       }	d d d «       	st        dd¬	«      ‚t%        |d
   «      D �
ci c]  }
|
d   |
“Œ
 }}
| j&                  D �]õ  }|j(                  j+                  d«      r¶|j-                  |j(                  «      }|€t        dd¬	«      ‚t/        |j0                  t3        |d   «      z
  «      dkD  rt        dd¬	«      ‚|j4                  rt        dd¬	«      ‚t7        j8                  |d
   ||j:                  «      €t        dd|j:                  › �¬	«      ‚ŒÕt        j<                  j-                  |j(                  «      }|€t        dd|j(                  › �¬	«      ‚t/        |j0                  t3        ||   «      z
  «      dkD  rt        dd¬	«      ‚|j4                  |j(                  t        j>                  v k7  rt        dd¬	«      ‚|j4                  r(|j:                  j+                  d«      st        dd¬	«      ‚t7        j8                  |d
   ||j:                  «      ��ŒÞt        dd|j:                  › �¬	«      ‚ tA        d„ | j&                  D «       «      }t/        || jB                  z
  «      dkD  rt        dd ¬	«      ‚tE        tG        jH                  «       «      }tK        «       }| j&                  D �cg c]  }|jM                  «       ‘Œ }}d }d }tN        ro	 tQ        jR                  tT        jV                  jX                  tN        t[        |d!z  «      d"|||d
   d#œ|d$d%i¬&«      ƒ d {  –—† }|j\                  }|j^                  }ntb        jk                  d,«       t        «       5 }|j!                  d-||d.||| jl                  t[        |d/«      d0to        jp                  |d1¬2«      |d3||d
   f«       |js                  «        d d d «       tb        ju                  d4|||«       tw        jx                  d5|d6||d
   ||t{        | j&                  «      d7œd8¬9«       ||d%d:œS # 1 sw Y   �ŒxY wc c}
w c c}w 7 �Œ# t`        $ rH}tb        je                  d'|«       tg        jh                  d(d)tE        |«      «       t        d*d+¬	«      ‚d }~ww xY w# 1 sw Y   ŒËxY w­w);Nzorder-create:r©   rª   r  rá   r  rD  z5Le consentement RGPD est requis avant toute commande.rç   rè   Úonline_paymentr'  u9   Le paiement en ligne n'est pas activÃ© pour cette Ã©cole.é�  zAdresse e-mail invalide.r¬   z9SELECT 1 FROM school_classes WHERE school_id=? AND name=?u"   Classe inconnue pour cette Ã©cole.rÆ   rñ   u(   Produit inconnu ou retirÃ© du catalogue.rõ   gü©ñÒMbP?u-   Les tarifs ont changÃ© â€” rechargez la page.u   Format incohÃ©rent.zPhoto introuvable : zFormat inconnu : r   uA   Une photo magique doit Ãªtre gÃ©nÃ©rÃ©e avant d'Ãªtre commandÃ©e.c              3   óN   K  — | ]  }|j                   |j                  z  –— Œ y ­w©N)rÎ   rÍ   )r!  r  s     r7   r"  zcreate_order.<locals>.<genexpr>¶  s   è ø€ ÒD°2�2—=‘= 2§;¡;Õ.ÑDùs   ‚#%g{®Gáz„?u   Total incohÃ©rent.éd   Úeur)Úorder_idrÖ   rè   ÚenabledT)Úapi_keyÚamountÚcurrencyÚmetadataÚreceipt_emailÚautomatic_payment_methodszStripe PaymentIntent error: %szstripe-intent-failedu.   Stripe : crÃ©ation de PaymentIntent impossibleiö  u7   Le service de paiement est momentanÃ©ment indisponible.uO   STRIPE_SECRET_KEY missing â€” order created without payment (development only).zþINSERT INTO orders (id, family_code, customer_name, class_name, customer_email, shipping_address,
               total, status, items_json, created_at, payment_status, stripe_payment_intent_id, school_id)
               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)r�   r  ÚpendingF)Úensure_asciiÚunpaidz&Order created: %s family=%s total=%.2fzsale.createÚorder)rÖ   rÝ   rÜ   r2   )r  rT  rU  rè   r  r  )ri  Úclient_secretÚsuccess)>rä   rÖ   r   r±   rî   r   r)   r1  rÙ   ÚstripÚ	_EMAIL_REÚmatchrT   r×   r­   r   r/   rì   rý   rÜ   rÆ   r„   r   ÚabsrÎ   rÒ   rÏ   r$   Úsafe_photo_pathrÄ   ÚFORMAT_PRICE_KEYÚMAGIC_FORMATSr3  rÝ   r£   ÚuuidÚuuid4r!   Ú
model_dumpr   r[   rJ  ÚstripeÚPaymentIntentÚcreateÚroundru  rð   r  r1   Úerrorr#   r½   rU   rÚ   ÚjsonÚdumpsr0   r2   r"   r0  Úlen)r§   r�   rá   r8  rí   r:  Úemailr×   r6   Úknownrc   Ú	cataloguer  ÚproductÚ	price_keyÚcomputedri  rV  Ú
items_dataru  Úpayment_intent_idÚintentÚes                          r7   Úcreate_orderr“  …  sK  è ø€ ä�d×&Ñ&Ó'€DÜ	�7Ó	€BÜ˜ r dÐ+¸"ÈTÕRÜ
�dÓ
€CØˆv‰;€DØˆ|ÒÜ˜CÐ(_Ô`Ð`ä×*Ñ*¨3¨{Ñ+;Ó<€HØÐ Ñ! VÒ+Ü˜CÐ(cÔdÐdØ×Ñ×%Ñ%Ó'€EÜ�?‰?˜5Ô!Ü˜CÐ(BÔCÐCØ—‘˜$Ÿ/™/×/Ñ/Ó1Ó2€JÜ	‹ð H�TØ—‘ÐXØ! +Ñ.°
Ð;ó=ß=E¹X»Zð 	÷Hñ Ü˜CÐ(LÔMÐMô -=¸SÀÑ=MÓ,NÖO q��;‘ Ñ"ÐO€IÐOØ�j‰jó XˆØ�<‰<×"Ñ" :Ô.Ø—m‘m B§L¡LÓ1ˆGØˆÜ# CÐ0ZÔ[Ð[Ü�2—=‘=¤5¨°Ñ)9Ó#:Ñ:Ó;¸eÒCÜ# CÐ0_Ô`Ð`Ø�{Š{Ü# CÐ0EÔFÐFÜ×,Ñ,¨S°Ñ-=¸tÀR×EVÑEVÓWÐ_Ü# CÐ2FÀr×GXÑGXÐFYÐ0ZÔ[Ð[ØÜ"×3Ñ3×7Ñ7¸¿¹ÓEˆ	ØÐÜ Ð.?ÀÇÁ¸~Ð,NÔOÐOÜˆr�}‰}œu X¨iÑ%8Ó9Ñ9Ó:¸UÒBÜ Ð,[Ô\Ð\Ø�;‰;˜2Ÿ<™<¬>×+GÑ+GÐGÒHÜ Ð,AÔBÐBØ�;Š;˜r×0Ñ0×;Ñ;¸EÔBÜ Ð,oÔpÐpÜ×(Ñ(¨¨[Ñ)9¸4À×ARÑARÓSÒ[Ü Ð.BÀ2×CTÑCTÐBUÐ,VÔWÐWð-Xô0 ÑD¸¿¹ÔDÓD€HÜ
ˆ8�d—j‘jÑ Ó! DÒ(Ü˜CÐ(<Ô=Ð=ä”4—:‘:“<Ó €HÜ
‹,€CØ,0¯J©JÖ7 b�"—-‘-•/Ð7€JÐ7ð €MØÐÝð	gÜ"×,Ñ,Ü×$Ñ$×+Ñ+Ü)Ü˜X¨™^Ó,ØØ&.¸tÐRUÐVaÑRbÑcØ#Ø+4°dÐ*;ô÷ ˆFð #×0Ñ0ˆMØ &§	¡	Ñô 	�‰ÐeÔfä	‹ð �TØ�‰ð5ð �t˜R ¨U°D×4IÑ4IÌ5ÐQYÐ[\ÓK]Ð_hÜ�Z‰Z˜
°Ô7¸¸hÐHYÐ[^Ð_jÑ[kðmô		
ð 	�‰Œ÷ô ‡H�HÐ5°xÀÀxÔPÜ×ÑØ "°GÈÐ]`ÐalÑ]mØ $¨xÄ#ÀdÇjÁjÃ/ÑRÐ]cõeð !°=ÈTÑRÐR÷[Hñ Hüò Pùò> 8ðúô ò 	gÜ�I‰IÐ6¸Ô:Ü× Ñ Ð!7Ð9iÔknÐopÓkqÔrÜ Ð,eÔfÐfûð	gú÷ð üs�   ‚C0V/Ã2&T5Ä'V/Ä?UÅG*V/Ì8BV/ÏUÏ#V/Ï0AU Ñ UÑU Ñ V/Ñ=AV#ÓAV/Ô5T?Ô:V/ÕU Õ	V ÕAVÖV Ö V/Ö#V,Ö(V/c                   ó^   — e Zd ZU  ed eej                  dz  dz  «      dz   ¬«      Zee	d<   y)ÚScanRequestr©   é   rØ   rg  rÂ   ÚimageN)
rŠ   r‹   rŒ   r   rÑ   r   ÚSCAN_MAX_IMAGE_BYTESr—  r£   r¤   rŽ   rF   r7   r•  r•  è  s0   … Ù "±°V×5PÑ5PÐSTÑ5TÐWXÑ5XÓ1YÐ\_Ñ1_Ô`€Eˆ3Ô`rF   r•  g        ©ÚatÚvalueÚ_prefix_cachec            	      ó`  — ddl } ddlm} | j                  «       }|t        d   z
  dkD  r}t        «       }t        j                  d¬«      D ]C  }||j                  t        j                  |d   «      t        j                  |d   «      «      z  }ŒE t        j                  ||¬	«       t        d
   S )u¯   Prefijos de cÃ³digo de todos los colegios activos (config + carpetas en disco) para el OCR del escaneo.
    En cachÃ© 60 s: recorrer el disco en cada fotograma serÃ­a absurdo.r   N©Ú	qr_detectrš  é<   F)Úinclude_inactiverð   r™  r›  )ÚtimerA   rŸ  Ú	monotonicrœ  rÿ   r)   Úlist_schoolsÚvalid_prefixesÚcode_prefix_forr%   Úschool_rootÚupdate)r¢  rŸ  rV  ÚprefixesÚss        r7   Ú_known_prefixesr«  ï  s¥   € ó Ý"Ø
�.‰.Ó
€CØ
Œ]˜4Ñ Ñ  2Ò%Ü›ˆÜ×,Ñ,¸eÔDò 	~ˆAØ˜	×0Ñ0´×1OÑ1OÐPQÐRVÑPWÓ1XÔZg×ZsÑZsÐtuÐvzÑt{ÓZ|Ó}Ñ}‰Hð	~ä×Ñ ¨8ÐÔ4Ü˜Ñ!Ð!rF   z/api/portal/scanc              ƒ   óö   K  — t        dt        |«      › �dd¬«       t        j                  t        «      ƒ d{  –—† }t        j                  t
        j                  | j                  |«      ƒ d{  –—† S 7 Œ;7 Œ­w)u˜  Fotograma de la cÃ¡mara del mÃ³vil â†’ cÃ³digo de familia (OpenCV completo + OCR del cÃ³digo impreso).

    Misma idea que `/api/scan/frame` de hec-photo-app: el navegador prueba antes con jsQR (gratis, en local)
    y solo manda aquÃ­ los fotogramas que no ha podido leer. Devuelve Ãºnicamente el texto leÃ­do en la tarjeta;
    no consulta la base, asÃ­ que no revela si el cÃ³digo existe. Limitado por IP.zportal-scan:rÇ   r°   r  N)r±   r   r[   rJ  r«  r(   Ú
scan_framer—  )r§   r�   r©  s      r7   Úportal_scanr®  ý  s`   è ø€ ô ˜¤i°Ó&8Ð%9Ð:ÈÐ]`ÕaÜ×&Ñ&¤Ó7×7€HÜ×"Ñ"¤<×#:Ñ#:¸D¿J¹JÈÓQ×QÐQð 8øØQús!   ‚7A9¹A5º6A9Á0A7Á1A9Á7A9z	/r/{code}c              ƒ   óÚ   K  — ddl m} ddlm} |j	                  | «      }|st        dd¬«      ‚|j                  j                  dd«      } |t        j                  › d	|› d
|› �d¬«      S ­w)uD  Punto de entrada de las URLs embebidas en el QR (`https://dominio/r/CÃ“DIGO`) â†’ portal con el cÃ³digo.

    El esquema es HTTPS por defecto: tras un proxy inverso `request.url.scheme` llega como http (la conexiÃ³n
    interna), y no todos los vhost de :80 redirigen a HTTPS (QR_REDIRECT_SCHEME lo cambia si hiciera falta).r   rž  )ÚRedirectResponseræ   zCode introuvable.rç   Úhostr�   z://z/?code=i.  )r–   r¦   )
rA   rŸ  Úfastapi.responsesr°  Úextract_coder   r”   r   r   ÚQR_REDIRECT_SCHEME)rá   r�   rŸ  r°  Úcleanr±  s         r7   Úqr_redirectr¶  	  sk   è ø€ õ #Ý2Ø×"Ñ" 4Ó(€EÙÜ˜CÐ(;Ô<Ð<Ø�?‰?×Ñ˜v rÓ*€DÙ¤6×#<Ñ#<Ð"=¸SÀÀÀgÈeÈWÐ UÐcfÔgÐgùr´   z/api/stripe/webhookc              ƒ   óÒ  K  — | j                  «       ƒ d {  –—† }| j                  j                  dd«      }t        r'	 t        j
                  j                  ||t        «      }n>t        rt        d
d¬	«      ‚t        j                  d«       	 t        j                  |«      }d"d„} ||d«      } | ||d«      d«      } ||d«      }	 |dk(  r't!        | ||d«      xs	  ||d«      «      ƒ d {  –—†  n|dk(  rt#        |«      ƒ d {  –—†  d d!iS 7 Œú# t        $ r?}t        j                  d|«       t        j                  ddd«       t        dd¬	«      ‚d }~ww xY w# t        $ r t        dd¬	«      ‚w xY w7 Œˆ7 Œq# t        $ r_}t        j%                  d|«       t        j                  dd|› d�d|› dt'        |«      j(                  › d|› �«       t        dd¬	«      ‚d }~ww xY w­w)#Nzstripe-signaturer�   z/Stripe webhook rejected (invalid signature): %szstripe-webhook-signatureu+   Webhook Stripe rejetÃ© (signature invalide)uT   VÃ©rifiez que STRIPE_WEBHOOK_SECRET correspond Ã  l'endpoint configurÃ© dans Stripe.rd  zSignature invalide.rç   r  u   Webhook non configurÃ©.uL   STRIPE_WEBHOOK_SECRET missing â€” signature not verified (development only).zPayload invalide.c                 ó`   — t        | t        «      r| j                  ||«      S t        | ||«      S rf  )Ú
isinstanceÚdictr   Úgetattr)Úobjr9  rÉ   s      r7   Úgzstripe_webhook.<locals>.g0  s,   € Ü(2°3¼Ô(=ˆs�w‰w�s˜GÓ$Ð]Ä7È3ÐPSÐU\ÓC]Ð]rF   r�   ÚdataÚobjectrð   zpayment_intent.succeededÚamount_receivedrl  zpayment_intent.payment_failedzWebhook %s processing failedzstripe-webhook-processingzWebhook Stripe u
    en Ã©checzpayment_intent=z : r·   rž   u,   Traitement en Ã©chec â€” Stripe rÃ©essaiera.ÚreceivedTrf  )r§   r”   r   r   r�  ÚWebhookÚconstruct_eventr  r1   rU   r#   r½   r   r   r†  ÚloadsÚ_mark_order_paidÚ_mark_order_failedr»   r�   rŠ   )	r�   ÚpayloadÚ
sig_headerÚeventr’  r½  Ú
event_typeÚdata_objectÚpi_ids	            r7   Ústripe_webhookrÍ    só  è ø€ à—L‘L“N×"€GØ—‘×$Ñ$Ð%7¸Ó<€Jåð	CÜ—N‘N×2Ñ2°7¸JÔH]Ó^‰Eõ 
Ü˜CÐ(AÔBÐBä�‰ÐbÔcð	AÜ—J‘J˜wÓ'ˆEó^ñ �5˜&Ó!€JÙ‘A�e˜VÓ$ hÓ/€KÙˆk˜4Ó €Eð	XØÐ3Ò3Ü" 5©!¨KÐ9JÓ*KÒ*gÉqÐQ\Ð^fÓOgÓh×hÑhØÐ:Ò:Ü$ UÓ+×+Ð+ð ˜ÐÐðK #ùô ò 	CÜ�K‰KÐIÈ1ÔMÜ× Ñ Ð!;Ð=jØ!wôyä Ð,AÔBÐBûð		Cûô ò 	AÜ Ð,?Ô@Ð@ð	Aúð iøà+ùÜò XÜ�‰Ð4°jÔAÜ×ÑÐ8¸OÈJÈ<ÐWaÐ:bØ.¨u¨g°S¼¸a»×9IÑ9IÐ8JÈ"ÈQÈCÐPô	Rä˜CÐ(VÔWÐWûð	Xüs“   ‚G'–D—&G'¾%D Á#)G'ÂE Â"&G'Ã	&E< Ã/E8Ã0E< ÄE:Ä	E< ÄG'Ä	EÄ:EÅEÅG'ÅE5Å5G'Å8E< Å:E< Å<	G$ÆAGÇG$Ç$G'Úentitledc                 óˆ  — t         j                  dz  dz  }g }d}|D ]¢  }t        j                  | ||d   «      }|€t        j                  d||d   «       Œ:|j                  «       }|t        |«      z  }||kD  r)t        j                  d|t         j                  «       g c S |j                  |j                  |f«       Œ¤ |S )ul  Read the paid HD originals to attach to the confirmation e-mail.

    All-or-nothing on size: a partial set would look like the order was short-changed, so if the total
    exceeds EMAIL_ATTACH_MAX_MB nothing is attached and the family uses the portal download instead.
    A missing file (already purged) is skipped, never fatal â€” the payment is already booked.r{   r   rÄ   z-Attachment skipped, original not found: %s/%suD   Originals for %s exceed %d MB â€” not attached, portal download only)r   ÚEMAIL_ATTACH_MAX_MBr$   r{  r1   rU   Ú
read_bytesrˆ  r2   rZ   rM   )	rè   rá   rÎ  ÚlimitÚfilesrÝ   r  rƒ   r¾  s	            r7   Ú_collect_attachmentsrÔ  D  sÆ   € ô ×&Ñ&¨Ñ-°Ñ4€EØ%'€EØ€EØò 
(ˆÜ×,Ñ,¨Y¸¸bÐAQÑ>RÓSˆØˆ<Ü�K‰KÐGÈÈrÐRbÑOcÔdØØ�‰Ó ˆØ”�T“ÑˆØ�5Š=Ü�H‰HÐ[Ð]aÔci×c}Ñc}Ô~ØŠIØ�‰�d—i‘i Ð&Õ'ð
(ð €LrF   r�  Úamount_centsc              ƒ   óÌ  K  — t        «       5 }|j                  d| f«      j                  «       }|s t        j	                  d| «       	 d d d «       y |�`t        |«      t        |d   dz  «      k7  rCt        j                  d|d   › �dd|d   › d	t        |d   dz  «      › d
|› d�«       	 d d d «       y |j                  d|d   f«      }|j                  «        |j                  dkD  }d d d «       sy d   |d   }}t        j                  |d   «      }t        j                  d|||d   «       t        j                  dd||d   ||d   dœd¬«       t         rúddlm}	m}
 |d   rt)        j*                  |d   «      ni }|j-                  dd«      }|r|j/                  d«      sd|z   }t1        |«      }t3        j4                  t6        |d   xs d||«      ƒ d {  –—† }t3        j8                   |	|d   d||||d   t;        |«      ||¬«	      «       t3        j8                   |
|||d   xs d|d   ||d   |j-                  dd«      ¬ «      «       y y # 1 sw Y   �ŒoxY w7 Œ…­w)!Nú5SELECT * FROM orders WHERE stripe_payment_intent_id=?z4Webhook succeeded pour un payment_intent inconnu: %srÝ   rg  zstripe-amount-mismatch:rð   u3   Paiement Stripe : montant diffÃ©rent de la commandez	commande z	 attendu u    centimes, reÃ§u u,   . La commande n'a PAS Ã©tÃ© marquÃ©e payÃ©e.z“UPDATE orders SET payment_status='paid', status=CASE WHEN status='cancelled' THEN 'pending' ELSE status END WHERE id=? AND payment_status != 'paid'r   rÖ   Ú
items_jsonz#Order paid: %s family=%s total=%.2fz	sale.paidrt  rè   )rÖ   rÝ   r2   )rT  rU  rè   r  r  )Úsend_order_confirmationÚsend_company_notificationÚ
portal_urlr�   r‚   úhttps://rÙ   )	ÚtorM   ri  rÖ   rÜ   rÝ   Úhas_digitalrÛ  Úattachmentsr×   Únotification_email)ri  rÖ   Úcustomer_namerÙ   rÜ   rÝ   rÝ  )r   r/   rì   r1   rU   rÑ   r„  r#   r½   r0   Úrowcountr†  rÄ  r2   r"   r0  r+   Úservices.email_servicerÙ  rÚ  r)   r1  r   r„   r  r[   rJ  rÔ  r\   rÓ   )r�  rÕ  r6   rW  ÚcurÚ
newly_paidri  rá   rÜ   rÙ  rÚ  r:  rÛ  rÎ  rß  s                  r7   rÅ  rÅ  [  s   è ø€ Ü	‹ð &�TØ�l‰lÐRÐUfÐThÓi×rÑrÓtˆÙÜ�K‰KÐNÐPaÔbØ÷	&ð &ð
 Ð#¬¨LÓ(9¼UÀ3ÀwÁ<ÐRUÑCUÓ=VÒ(VÜ× Ñ Ø)¨#¨d©)¨Ð5Ð7lØ˜C ™I˜; i´°c¸'±lÀSÑ6HÓ0IÐ/JÐJ[Ð\hÐ[ið j=ð >ô?ð ÷&ð &ð �l‰lð6Ø8;¸D¹	°|óEˆð 	�‰ŒØ—\‘\ AÑ%ˆ
÷&ñ  Øà˜‘Y  MÑ 2ˆd€HÜ�J‰J�s˜<Ñ(Ó)€EÜ‡H�HÐ2°H¸dÀCÈÁLÔQÜ×Ñ˜K°wÈHÐ`cÐdoÑ`pØ48À3ÀwÁ<Ñ$PÐ[aõcõ ß]ØDGÈÒDT”>×.Ñ.¨s°;Ñ/?Ô@ÐZ\ˆØ—\‘\ ,°Ó3ˆ
Ù˜j×3Ñ3°FÔ;Ø# jÑ0ˆJÜ% eÓ,ˆÜ#×-Ñ-Ô.BÀCÈÑDTÒDZÐXZÐ\`ÐbjÓk×kˆÜ×ÑÑ3ØÐ#Ñ$¨2¸ÈdØ˜s 7™|¼¸h»ÐT^Ø#ô%ô 	&ô 	×ÑÑ5Ø¨4¸sÀ<Ñ?PÒ?VÐTVØÐ/Ñ0¸ÀSÈÁ\Ø�|‰|Ð0°"Ó5ô7õ 	8ð ÷3&ñ &úð@ lúsC   ‚I$�;IÁ	I$ÁAIÂ*	I$Â35IÃ(C4I$ÇI"ÇA8I$ÉIÉ	I$)Úcleanup_servicec              ƒ   ó6  K  — t        «       5 }|j                  d| f«      j                  «       }|r|d   dk(  r
	 d d d «       y |d   dk(  }|j                  d|d   f«       |j                  «        d d d «       t        j                  d| «       t        rwstdd	lm} d
   rt        j                  |d
   «      ni }|j                  dd«      }|r|j                  d«      sd|z   }t        j                   ||d   d|d   |«      «       y y y # 1 sw Y   Œ�xY w­w)Nr×  Úpayment_statusÚpaidÚfailedz4UPDATE orders SET payment_status='failed' WHERE id=?rð   z$Payment failed for payment_intent=%sr   )Úsend_payment_failedrè   rÛ  r�   r‚   rÜ  rÙ   )r   r/   rì   r0   r1   r2   r+   rã  rë  r)   r1  r   r„   r[   r\   )r�  r6   rW  Ú
was_failedrë  r:  rÛ  s          r7   rÆ  rÆ  ‹  s  è ø€ Ü	‹ð �TØ�l‰lÐRÐUfÐThÓi×rÑrÓtˆÙ�cÐ*Ñ+¨vÒ5Ø÷ð ð Ð)Ñ*¨hÑ6ˆ
Ø�‰ÐKÈcÐRVÉiÈ\ÔZØ�‰Œ÷ô ‡H�HÐ3Ð5FÔGÝ™ZÝ>ØDGÈÒDT”>×.Ñ.¨s°;Ñ/?Ô@ÐZ\ˆØ—\‘\ ,°Ó3ˆ
Ù˜j×3Ñ3°FÔ;Ø# jÑ0ˆJÜ×ÑÑ/°Ð4DÑ0EÀrÈ3ÈtÉ9ÐV`ÓaÕbð (€}÷ð üs(   ‚D�-Dº	DÁ.DÁ1BDÄDÄDc              ƒ   óØ  K  — t        |«      }t        dt        | «      › �dd¬«       t        «       5 }|j	                  d|f«      j                  «       }d d d «       g }D ]ö  }t        j                  |d   «      }g }|d   dk(  rs|d	   d
k7  rkt        |«      D ]]  }|j                  |d   |j                  d«      xs |j                  d«      xs |d   t        j                  |d   |d   «      dœ«       Œ_ |j                  |d   |d   |d   |d   xs d|d   |d	   |d   |||d   t        |«      dkD  rt        j                  |d   «      nd dœ«       Œø d|iS # 1 sw Y   �ŒxY w­w)Nzorders-lookup:r   r°   r  zCSELECT * FROM orders WHERE family_code = ? ORDER BY created_at DESCrØ  rè  ré  r  Ú	cancelledrÄ   rÐ   rÊ   rð   )r  Úlabelr–   rÖ   rÙ   r×   r�   rÝ   Ú
created_atrÁ   )rð   rÖ   rÙ   r×   rÝ   r  rè  rÜ   Ú	downloadsrð  Úzip_urlÚorders)rä   r±   r   r   r/   rú   r†  rÄ  r  rZ   r   r$   Údownload_urlrˆ  Údownload_zip_url)	r�   rá   r6   rû   ró  rW  rÜ   rñ  r  s	            r7   Ú
get_ordersrö  Ÿ  s£  è ø€ ä�dÓ€DÜ˜¬	°'Ó(:Ð';Ð<È2Ð^aÕbÜ	‹ð w�TØ�|‰|ÐaÐdhÐcjÓk×tÑtÓvˆ÷wð €FØò ˆÜ—
‘
˜3˜|Ñ,Ó-ˆØˆ	ØÐÑ  FÒ*¨s°8©}ÀÒ/KÜ(¨Ó/ò �Ø× Ñ Ø "Ð#3Ñ 4ØŸV™V KÓ0Òb°B·F±F¸>Ó4JÒbÈbÐQaÑNbÜ(×5Ñ5°c¸$±iÀÐDTÑAUÓVñ"õ ðð 	�‰Ø�d‘)¨C°Ñ,>Ø!Ð"2Ñ3À3À|ÑCTÒCZÐXZØ˜‘\¨S°©]ÈcÐRbÑNcØ¨À#ÀlÑBSÜDGÈ	ÃNÐUVÒDV”}×5Ñ5°c¸$±iÔ@Ð\`ñ
õ 	ðð" �fÐÐ÷+wñ wüs   ‚0E*²"EÁD	E*ÅE'Å"E*z*/api/portal/download/{order_id}/{filename}ri  c           
   ƒ   óŠ  K  — t        j                  | |||«      st        dd¬«      ‚t        «       5 }|j	                  d| f«      j                  «       }d d d «       r|d   dk7  s|d   dk(  rt        dd	¬«      ‚t        j                  |d
   «      }|t        |«      D �ch c]  }|d   ’Œ	 c}vrt        dd¬«      ‚t        j                  |d   xs d|d   |«      }	|	€t        dd¬«      ‚t        j                  dt        |«      d| d|id|d   ¬«       t        |	|	j                  ddi¬«      S # 1 sw Y   ŒßxY wc c}w ­w)NrD  u/   Lien expirÃ© â€” rechargez Â« Mes commandes Â».rç   úSELECT * FROM orders WHERE id=?rè  ré  r  rî  u   Commande non payÃ©e.rØ  rÄ   uB   Cette commande ne comprend pas le tÃ©lÃ©chargement de cette photo.rè   r�   rÖ   ræ   uA   Photo indisponible (supprimÃ©e aprÃ¨s la durÃ©e de conservation).zphoto.downloadrt  Úfiler2   ©r  rT  rU  r  r  rè   r“   úprivate, no-store)r  r”   )r$   Úcheck_download_signaturer   r   r/   rì   r†  rÄ  r  r{  r"   r0  r   r   rM   )
ri  r  r�   r@  rA  r6   rW  rÜ   r  rƒ   s
             r7   Údownload_originalrý  »  sV  è ø€ ô ×1Ñ1°(¸HÀcÈ3ÔOÜ˜CÐ(YÔZÐZÜ	‹ð V�TØ�l‰lÐ<¸x¸kÓJ×SÑSÓUˆ÷Vá�#Ð&Ñ'¨6Ò1°S¸±]ÀkÒ5QÜ˜CÐ(>Ô?Ð?Ü�J‰J�s˜<Ñ(Ó)€EØÔ7IÈ%Ó7PÖQ°˜Ð+Ó,ÒQÑQÜ˜CÐ(lÔmÐmÜ×(Ñ(¨¨[Ñ)9Ò)?¸RÀÀ]ÑASÐU]Ó^€DØ€|Ü˜CÐ(kÔlÐlÜ×ÑÐ,¼À7Ó9KÐ[bØ(0¸6À8Ð:LÐW]Ø&)¨+Ñ&6õ8ô ˜ t§y¡y¸?ÐL_Ð:`ÔaÐa÷Vð Vüò
 Rùs*   ‚0E²"D2ÁAEÂ!D>Â-BEÄ2D;Ä7Ez#/api/portal/download-zip/{order_id}c           
   ƒ   óâ  ‡‡K  — t        j                  | ||«      st        dd¬«      ‚t        «       5 }|j	                  d| f«      j                  «       Šddd«       ‰r‰d   dk7  s‰d   d	k(  rt        dd
¬«      ‚t        t        t        j                  ‰d   «      «      D �ch c]  }|d   ’Œ	 c}«      }ˆfd„|D «       D �cg c]  }|sŒ|‘Œ	 }}|st        dd¬«      ‚ddl
}	ddl}
ddlm} |	j                  ddd¬«      Š‰j                  «        |
j!                  ‰j"                  d|
j$                  «      5 }|D ]  }|j'                  ||j"                  ¬«       Œ! 	 ddd«       t)        j*                  dt-        |«      d| dt/        |«      id‰d   ¬«       t1        ‰j"                  d‰d   › d | dd! › d�d"d#i |ˆfd$„«      ¬%«      S # 1 sw Y   �ŒyxY wc c}w c c}w # 1 sw Y   Œ„xY w­w)&zoAll purchased originals of one paid order in a single ZIP (JPEGs are already compressed: stored, not deflated).rD  u(   Link expired â€” reload â€œMy ordersâ€�.rç   rø  Nrè  ré  r  rî  zOrder not paid.rØ  rÄ   c              3   ó`   •K  — | ]%  }t        j                  ‰d    xs d‰d   |«      –— Œ' y­w)rè   r�   rÖ   N)r$   r{  )r!  ÚnrW  s     €r7   r"  z%download_order_zip.<locals>.<genexpr>Ú  s2   øè ø€ ÒuÐjkœ×6Ñ6°s¸;Ñ7GÒ7MÈ2ÈsÐS`ÑOaÐcd×eÑuùs   ƒ+.ræ   z8Photos unavailable (deleted after the retention period).r   )ÚBackgroundTaskzmeec-z.zipF)ÚprefixÚsuffixÚdeleteÚw)Úarcnamezphoto.download_ziprt  rÓ  r2   rè   rú  z	meeconic-rÖ   ú-é   r“   rû  c                  óB   •— t        j                  ‰ j                  «      S rf  )ÚosÚunlinkrM   )Útmps   €r7   r¯   z$download_order_zip.<locals>.<lambda>ê  s   ø€ ¼"¿)¹)ÀCÇHÁHÓ:M€ rF   )r  r”   Ú
background)r$   Úcheck_download_zip_signaturer   r   r/   rì   r^  r  r†  rÄ  ÚtempfileÚzipfileÚstarlette.backgroundr  ÚNamedTemporaryFileÚcloseÚZipFilerM   Ú
ZIP_STOREDÚwriter"   r0  r   rˆ  r   )ri  r�   r@  rA  r6   r  r`  rc   Úpathsr  r  r  ÚzfrW  r  s                @@r7   Údownload_order_zipr  Ð  sï  ùè ø€ ô ×5Ñ5°hÀÀSÔIÜ˜CÐ(RÔSÐSÜ	‹ð V�TØ�l‰lÐ<¸x¸kÓJ×SÑSÓUˆ÷Vá�#Ð&Ñ'¨6Ò1°S¸±]ÀkÒ5QÜ˜CÐ(9Ô:Ð:ÜÔ3EÄdÇjÁjÐQTÐUaÑQbÓFcÓ3dÖe¨R�BÐ'Ó(ÒeÓf€EÛuÐotÔuÖ{�1ÒyzŠQÐ{€EÐ{ÙÜ˜CÐ(bÔcÐcÛÛÝ3Ø
×
%Ñ
%¨W¸VÈEÐ
%Ó
R€CØ‡I�I„KØ	�‰˜Ÿ™ 3¨×(:Ñ(:Ó	;ð (¸rØò 	(ˆAØ�H‰H�Q §¡ˆHÕ'ñ	(÷(ô ×ÑÐ0¼YÀwÓ=OÐ_fØ(0¸7ÄCÈÃJÐ:OÐZ`Ø&)¨+Ñ&6õ8ô ˜Ÿ™¨Y°s¸=Ñ7IÐ6JÈ!ÈHÐUWÐVWÈLÈ>ÐY]Ð+^Ø!0Ð2EÐ FÙ#1Ó2MÓ#NôPð P÷'Vñ Vüò fùÚ{÷(ð (üsU   „/G/³"GÁAG/Â$GÂ0G/ÃGÃGÃA*G/Ä:%G#Å A,G/ÇGÇG/Ç#G,Ç(G/Ú__main__ÚUVICORN_RELOADÚWEB_CONCURRENCYÚ1zmain:apprÁ   )r±  ÚportÚreloadÚworkers)r-   N)†r�   r[   r†  r
  ÚloggingÚrer~  Ú
contextlibr   Útypingr   r�  Úfastapir   r   r   r   Úfastapi.middleware.corsr	   Ústarlette.middleware.gzipr
   r²  r   r   Úpydanticr   r   Úcorer   Úcore.configr   r   r   r   r   r   r   r   r   Úcore.dbr   r   r   Úcore.leaderr   Úcore.logging_setupr   r/  r   r±   Úcore.securityr   r    Úcore.timeutilr!   rA   r"   r#   r$   r%   r&   r'   r(   r)   Ú	getLoggerr1   Ú_boolr+   Úcompilerx  r8   rE   rf   rG   r=   rC   ÚiaÚ	ia_moduleÚ	shoot_apiÚinclude_routerÚrouterÚadd_middlewarery   Ú
middlewarerš   rœ   Úpostr³   Úexception_handlerr  r¾   rÀ   rÕ   rà   r£   rä   rî   rÞ   rº  rý   r  r   r  r=  rÑ   rM  rQ  rX  ra  r“  r•  rÿ   rœ  r¤   r«  r®  r¶  rÍ  ÚtupleÚbytesrÔ  rÅ  ræ  Úset_payment_reconcilerrÆ  rö  rý  r  rŠ   Úuvicornr  Úgetenvr   ÚrunrŽ   rF   r7   ú<module>rB     s²  ðòó  Û Û 	Û Û 	Û Ý *Ý ã ß :Ó :Ý 2Ý 4ß 8ß %å ÷÷ õ ÷ <Ñ ;Ý %Ý ,Ý 9ß ?Ý $÷÷ ó ñ
 „Ø€g×Ñ˜
Ó#€à�—‘˜_¨gÓ6€ØˆB�J‰JÐ4Ó5€	óbó2&ð  ð8˜ò 8ó ð8ñ< Ð)°7ÀXÙ,‘t°'ÀTÑ_lÑW[ð  sBôC€ó Û Û Ø × Ñ �<×&Ñ&Ô 'Ø × Ñ �9×#Ñ#Ô $Ø × Ñ �9×#Ñ#Ô $à × Ñ ØØØÚFÚ?ð ô ÷1ñ 1ð  × Ñ �>Ô "ð ‡��Óð Gò ó ðô )�)ô )ð ‡�Ð
¨3€Ó/ðl˜[ð l°7ò ló 0ðlð ×Ñ�yÓ!ð[ wð [°Yò [ó "ð[ôI�	ô Iô˜ô ô�Yô ð1�Sð 1˜Só 1ð
�có ð 
 ð 
¨¨T©
ó 
ð˜d 4™jð ¨T°$©Zó ð ‡�ˆÓñló ðlð ‡�Ð	ÓÙ38¸ÈÐWYÔ3Zñ &˜gð &¨Sò &ó ð&ðR ‡�Ð	/Ó0Ù=BÀ3»ZÑTYÐZ]ÓT^Ù$)¨&Ð:JÔ$Kñ	L˜cð 	L¨Sð 	L°sð 	LÈcð 	LØ!ò	Ló 1ð	Lð ‡�Ð	NÓOá',¨S£z¹eÀC»jñj sð j¸#ð jÀSð jÐTWð jØ!$ðjØ8;òjó Pðjð ‡�Ð
Ó ðD˜^ð D°gò Dó !ðDð$ ‡�Ð	ÓÙ"'¨¸ÀbÔ"Iñ 	6˜Cò 	6ó  ð	6ð ‡�Ð
Óð]SÐ/ð ]S¸'ò ]Só  ð]SôDa�)ô að !©3«5Ñ1€ˆtÓ 1ð"˜ó "ð ‡�Ð
ÓðR˜Kð R°'ò Ró ðRð ‡�ˆÓðh˜Cð h¨'ò hó ðhð  ‡�Ð
Ó ð& 'ò &ó !ð&ðR Cð ¨sð ¸dÀ4¹jð ÈTÐRWÐX[Ð]bÐXbÑRcÑMdó ð.)8¨cð )8ÀÈ#Áð )8ÐSWó )8õX %Ø &€× &Ñ &Ð'7Ô 8ðc°ð c¸ó cð( ‡�Ð	ÓÙ38¸ÈÐWYÔ3Zñ ˜gð ¨Sò ó ðð6 ‡�Ð	5Ó6á',¨S£z¹eÀC»jñb cð b°Sð bÀ7ð bØ!$ðbØ8;òbó 7ðbð( ‡�Ð	.Ó/ÙINÈsËÑ`eÐfiÓ`jñ P sð P°Wð PÀ3ð PÐZ]ò Pó 0ðPð< ˆzÒÛÛØˆV�\‰\Ð*¨GÓ4€FÙ�)�"—)’)Ð-¨sÓ3Ó4€GØ€G‡K‚K�
 ¨D¸ÉfÉÐZaÖbð rF   