Ë
    5IÎj©3  ã                   ó"  — U d 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	m
Z
 ddlmZ ddlmZ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  ej4                  d«      ZdZeD � ch c]  } | j;                  «       ’Œ c} Z ej>                  d«      Z d$de
e!   de
e!   de"fd„Z#dede$e
e!   e
e!   f   fd„Z%d%dede"de&de"de$e"e'f   f
d„Z(dede!de&fd„Z)de"fd„Z*de"fd„Z+da,g Z-e.e$e!e"f      e/d<   d&d„Z0de"fd „Z1de"fd!„Z2de"fd"„Z3d&d#„Z4yc c} w )'u^  
cleanup_service.py â€” mantenimiento periÃ³dico.

1. Suppression automatique des photos (RGPD mineurs) : images plus vieilles que la rÃ©tention DE L'Ã‰COLE
   de la famille (rÃ©glage Â« rgpd_days Â», modifiable dans l'admin ; dÃ©faut PHOTO_RETENTION_DAYS) dans
   PHOTOS_DIR (layout `photos/{Ã©cole}/{photographe}/{CODE}/` et l'ancien `photos/{CODE}/`), la corbeille
   et INCOMING_DIR. Chaque suppression est tracÃ©e (audit Â« critical Â») ; au-delÃ  de RGPD_ALERT_THRESHOLD
   fichiers, une alerte e-mail part.
2. AperÃ§us (rÃ©duits / visages floutÃ©s) et cache `.protect/` orphelins : on ne garde jamais une copie
   d'une photo qui n'existe plus.
3. Corbeille : purgÃ©e aprÃ¨s TRASH_RETENTION_DAYS, sans jamais dÃ©passer la rÃ©tention RGPD de l'Ã©cole.
4. Commandes jamais payÃ©es : annulÃ©es aprÃ¨s UNPAID_ORDER_TTL_HOURS (le PaymentIntent Stripe est annulÃ©) â€”
   sauf si Stripe indique qu'il est payÃ© : la commande est alors enregistrÃ©e comme payÃ©e (voir expire_unpaid_orders).
5. Purge des compteurs de rate limiting pÃ©rimÃ©s.

Tourne une fois au dÃ©marrage puis toutes les 24 h (sur le worker leader).
é    N)Ú	timedelta)ÚPath)ÚOptional)Úconfig)Ú
PHOTOS_DIRÚINCOMING_DIRÚ
THUMBS_DIRÚPHOTO_RETENTION_DAYSÚIMAGE_EXTENSIONS)Úget_db)Úutcnow)Úaudit_serviceÚalert_servicezmeeconic.cleanupé€Q z>^(?P<stem>.+?)\.(?P<kind>wm|pv|h[0-9a-f]{10})(?:\.\d+)?\.webp$Úfamily_codeÚ	school_idÚreturnc                 óè   — 	 ddl m} |rt        |j                  |«      d   «      S | r|j	                  | «      S 	 t        S # t
        $ r'}t        j                  d|| |«       Y d}~t        S d}~ww xY w)ua   RÃ©tention de l'Ã©cole (directe, ou celle de la famille) ; Ã  dÃ©faut, la valeur globale du .env.r   )Úschool_serviceÚ	rgpd_daysz+Per-school retention unreadable (%s/%s): %sN)	Úservicesr   ÚintÚget_settingsÚretention_days_for_familyÚ	ExceptionÚlogÚwarningr
   )r   r   r   Úes       ú3/var/www/photos/backend/services/cleanup_service.pyÚ_retention_daysr    '   s{   € ð^Ý+ÙÜ�~×2Ñ2°9Ó=¸kÑJÓKÐKÙØ!×;Ñ;¸KÓHÐHð ô  Ðøô ò ^Ü�‰ÐAÀ9ÈkÐ[\×]Ð]ÜÐûð^ús   ‚$A §A Á	A1Á
A,Á,A1Úrelc                 óÌ   — | j                   }|sy|d   dk(  r/t        |«      dk\  r
|d   |d   fS t        |«      dkD  r|d   nddfS t        |«      dk\  r
|d   |d   fS d|d   fS )	u   (Ã©cole, code famille) d'aprÃ¨s le chemin relatif Ã  PHOTOS_DIR.

      Ã©cole/photographe/CODE/â€¦            â†’ nouveau layout
      .trash/Ã©cole/lot/photographe/CODE/â€¦ â†’ corbeille
      CODE/â€¦                              â†’ ancien layout (l'Ã©cole se dÃ©duit de la base)
    ©NNr   z.trashé   é   é   Né   )ÚpartsÚlen)r!   r(   s     r   Ú_locater*   4   s…   € ð �I‰I€EÙØØˆQ�x�8ÒÜ'*¨5£z°Q¢��a‘˜% ™(Ð#ÐhÌÈUËÐVWÊ¸UÀ1ºXÐ]aÐcgÐ<hÐhÜ
ˆ5ƒz�Q‚Ø�Q‰x˜˜q™Ð!Ð!Ø��q‘ˆ>Ðó    Ú	directoryÚdefault_daysÚ	by_familyÚ
keep_depthc                 ór  — | j                  «       sdi fS t        j                  «       }d}i }i }| j                  d«      D ]Ê  }|j                  «       r |j                  j                  «       t        v sŒ4d}	|r9t        |j                  | «      «      }	|	|vr|	\  }
}t        ||
¬«      ||	<   ||	   }n|}	 |j                  «       j                  ||dz  z
  k  r4|j                  «        |dz  }|	r|	d   r|j                  |	d«      dz   ||	<   ŒÌ t#        | j                  d«      d¬	«      D ]L  }|j%                  «       sŒt'        |j                  | «      j(                  «      |kD  sŒ<	 |j+                  «        ŒN ||fS # t        $ r"}t        j!                  d||«       Y d}~�Œbd}~ww xY w# t        $ r Y ŒŒw xY w)
uö  Supprime les images trop vieilles. Si `by_family`, la rÃ©tention est celle de l'Ã©cole de chaque famille.
    Les dossiers vides sont Ã©laguÃ©s, sauf ceux Ã  moins de `keep_depth` niveaux : `incoming/{Ã©cole}/` est le
    dossier de dÃ©pÃ´t FTP de l'appareil, il doit survivre vide (incident : Ã©laguÃ© par ce nettoyage, l'organizer
    a enchaÃ®nÃ© des centaines d'erreurs Â« No such file or directory Â» et l'appareil ne pouvait plus dÃ©poser).
    Retourne (total supprimÃ©, {(Ã©cole, famille): n}).r   Ú*N©r   r   r   r%   zSuppression impossible %s: %sT©Úreverse)ÚexistsÚtimeÚrglobÚis_fileÚsuffixÚlowerÚ	_IMG_EXTSr*   Úrelative_tor    ÚstatÚst_mtimeÚunlinkÚgetÚOSErrorr   r   ÚsortedÚis_dirr)   r(   Úrmdir)r,   r-   r.   r/   ÚnowÚdeletedÚ
per_familyÚcacheÚfÚkeyÚschoolÚfamilyÚdaysr   Úds                  r   Ú_delete_oldrO   E   s£  € ð ×ÑÔØ�"ˆuˆÜ
�)‰)‹+€CØ€GØ€JØ€EØ�_‰_˜SÓ!ò ?ˆØ—	‘	” §¡§¡Ó 0´IÑ =ØØˆÙÜ˜!Ÿ-™-¨	Ó2Ó3ˆCØ˜%ÑØ!$‘�˜Ü,¸È6ÔR��c‘
Ø˜‘:‰DàˆDð	?Ø�v‰v‹x× Ñ  3¨°©Ñ#5Ò5Ø—‘”
Ø˜1‘�Ù˜3˜qš6Ø&0§n¡n°S¸!Ó&<¸qÑ&@�J˜s‘Oøð#?ô( �I—O‘O CÓ(°$Ô7ò ˆØ�8‰8�:œ#˜aŸm™m¨IÓ6×<Ñ<Ó=À
ÓJðØ—‘•	ðð �JÐÐøô ò 	?Ü�K‰KÐ7¸¸A×>Ò>ûð	?ûô ò Ùðús+   Â4AE<Å&F*Å<	F'ÆF"Æ"F'Æ*	F6Æ5F6Ú
photos_dirÚstemc                 óh   ‡— 	 t        ˆfd„| j                  «       D «       «      S # t        $ r Y yw xY w)Nc              3   ó^   •K  — | ]$  }|j                  «       sŒ|j                  ‰k(  –— Œ& y ­w©N)r8   rQ   )Ú.0rI   rQ   s     €r   ú	<genexpr>z#_original_exists.<locals>.<genexpr>p   s!   øè ø€ ÒO aÀ1Ç9Á9Å;�1—6‘6˜T•>ÑOùs   ƒ-™-F)ÚanyÚiterdirrA   )rP   rQ   s    `r   Ú_original_existsrY   n   s4   ø€ ðÜÓO¨:×+=Ñ+=Ó+?ÔOÓOÐOøÜò Ùðús   ƒ!% ¥	1°1c                  ój  — d} t        j                  «       �rt        j                  d«      D ]¾  }|j                  «       sŒt        j                  |j                  «      }|r|j                  d«      n|j                  }t        |xr |j                  d«      dk(  «      }	 t        |j                  j                  t         «      z  }|st        ||«      rŒ©	 |j                  «        | dz  } ŒÀ t#        t        j                  d«      d¬«      D ]$  }|j%                  «       sŒ	 |j'                  «        Œ& t        j                  «       rœt        j                  d	«      D ]„  }|j%                  «       sŒ|j)                  «       D ]M  }|j                  j+                  d
«      d   }t        |j                  |«      rŒ8	 |j                  «        | dz  } ŒO 	 |j'                  «        Œ† | S # t        $ r Y �ŒÆw xY w# t         $ r Y �ŒÖw xY w# t         $ r Y �Œw xY w# t         $ r Y Œ w xY w# t         $ r Y ŒÓw xY w)uc   Supprime aperÃ§us et cache `.protect/` dont l'original n'existe plus. Retourne le nombre supprimÃ©.r   r1   rQ   ÚkindÚwmr%   Tr3   z.protectÚ_heads_)r	   r5   r7   r8   Ú_PREVIEW_REÚmatchÚnameÚgrouprQ   Úboolr   Úparentr<   Ú
ValueErrorrY   r?   rA   rB   rC   rD   rX   Úsplit)ÚremovedrI   ÚmrQ   ÚlegacyÚ	originalsrN   Úprotects           r   Úprune_orphan_derivativesrk   u   sø  € à€GÜ×ÑÕÜ×!Ñ! #Ó&ò 	ˆAØ—9‘9”;ØÜ×!Ñ! !§&¡&Ó)ˆAÙ&'�1—7‘7˜6”?¨Q¯V©VˆDä˜!Ò7 §¡¨£°4Ñ 7Ó8ˆFðÜ&¨¯©×)=Ñ)=¼jÓ)IÑI�	ñ Ô-¨i¸Õ>ðØ—H‘H”JØ˜q‘L‘Gð	ô" œ
×(Ñ(¨Ó-°tÔ<ò 	ˆAØ�x‰x�zðØ—G‘G•Ið	ô ×ÑÔÜ!×'Ñ'¨
Ó3ò 	ˆGØ—>‘>Ô#ØØ—_‘_Ó&ò �Ø—v‘v—|‘| IÓ.¨qÑ1�Ü'¨¯©¸Õ=ðØŸ™œ
Ø 1™™ððØ—‘•ð	ð €Nøô= ò Úðûô ò Úðûô ò Úðûô #ò Ùðûô ò ÙðúsZ   Â!&G'ÃG7Ä#HÆ;HÇH&Ç'	G4Ç3G4Ç7	HÈHÈ	HÈHÈ	H#È"H#È&	H2È1H2c                  ó   — ddl m} m} d}	 |j                  «       D ]A  }t	        t
        j                  t        |d   ¬«      «      }|| j                  |d   |«      z  }ŒC 	 |S # t        $ r!}t        j                  d|«       Y d }~|S d }~ww xY w)Nr   )Úphoto_servicer   Úid)r   zTrash purge failed: %s)r   rm   r   Úlist_schoolsÚminr   ÚTRASH_RETENTION_DAYSr    Úpurge_trashr   r   r   )rm   r   ÚpurgedÚsrM   r   s         r   Úpurge_trashesru   £   sŽ   € ß6Ø€Fð1Ø×,Ñ,Ó.ò 	?ˆAÜ”v×2Ñ2´OÈaÐPTÉgÔ4VÓWˆDØ�m×/Ñ/°°$±¸Ó>Ñ>‰Fñ	?ð
 €Møô ò 1Ü�‰Ð,¨a×0Ð0Ø€Mûð1ús   ŒAA# Á#	BÁ,BÂBÚ_paid_at_stripec                 ó   — | a y rT   )Ú_payment_reconciler)Úfns    r   Úset_payment_reconcilerrz   µ   s   € àÑr+   c            	      ó
  — t        «       t        t        j                  ¬«      z
  j	                  «       } t        «       5 }|j                  d| f«      j                  «       }ddd«       d}t        j                  rddl	}t        j                  |_
        g }D ]·  }|d   }|rš|�˜	 |j                  j                  |«      }|d   dk(  rQt        j                  |t        |j!                  d«      xs |d	   «      f«       t"        j%                  d
|d   |«       Œ�|d   dk7  r|j                  j'                  |«       |j                  |d   «       Œ¹ |r\t        «       5 }|D ]  }	|j                  d|	f«       Œ |j+                  «        ddd«       t"        j-                  dt/        |«      «       t/        |«      S # 1 sw Y   �Œ\xY w# t(        $ r&}t"        j%                  d|d   ||«       Y d}~�ŒZd}~ww xY w# 1 sw Y   ŒrxY w)aõ  Cancels orders never paid after the TTL. Returns how many were cancelled.

    Incident behind the Stripe check: two orders were charged at Stripe (`succeeded`) but the webhook never
    reached us (STRIPE_WEBHOOK_SECRET empty), so this job marked them `expired`/`cancelled` and the family
    paid for photos that were then refused. An order is now expired only when Stripe says the PaymentIntent
    is NOT paid; when Stripe cannot be asked, the order is left for the next cycle rather than guessed.)ÚhourszƒSELECT id, stripe_payment_intent_id FROM orders WHERE payment_status IN ('unpaid','failed') AND status='pending' AND created_at < ?Nr   Ústripe_payment_intent_idÚstatusÚ	succeededÚamount_receivedÚamountuT   Order %s is paid at Stripe (%s) but was never confirmed â€” booking it, not expiringrn   ÚcanceledzAOrder %s left for the next cycle, Stripe could not confirm %s: %szIUPDATE orders SET status='cancelled', payment_status='expired' WHERE id=?z%d unpaid order(s) expired)r   r   r   ÚUNPAID_ORDER_TTL_HOURSÚ	isoformatr   ÚexecuteÚfetchallÚSTRIPE_SECRET_KEYÚstripeÚapi_keyÚPaymentIntentÚretrieverv   Úappendr   r@   r   r   Úcancelr   ÚcommitÚinfor)   )
ÚcutoffÚconnÚrowsrˆ   Ú	to_expireÚrÚpi_idÚpir   Úorder_ids
             r   Úexpire_unpaid_ordersr˜   º   sü  € ô ‹hœ¬×)FÑ)FÔGÑG×RÑRÓT€FÜ	‹ð y�TØ�|‰|ðbØdjÐclónçnvÑnvÓnxð 	÷yð
 €FÜ×ÒÛÜ×1Ñ1ˆŒà€IØò "ˆØÐ,Ñ-ˆÙ�VÐ'ð
Ø×)Ñ)×2Ñ2°5Ó9�Ø�h‘< ;Ò.Ü#×*Ñ*¨E´3°r·v±vÐ>OÓ7PÒ7`ÐTVÐW_ÑT`Ó3aÐ+bÔcÜ—K‘KÐ vÐxyÐz~Ñxð  BGô  HØØ�h‘< :Ò-Ø×(Ñ(×/Ñ/°Ô6ð 	×Ñ˜˜4™Õ!ð"ñ  Ü‹Xð 	˜Ø%ò w�Ø—‘ÐhÐksÐjuÕvðwà�K‰KŒM÷	ô 	�‰Ð-¬s°9«~Ô>Üˆy‹>Ð÷Cyñ yûô, ò Ü—‘Ð_ÐabÐcgÑahÐjoÐqrÔsÝûðú÷	ð 	ús6   ¾"F:Â$A3GÄ#GÅ+G9Æ:GÇ	G6ÇG1Ç1G6Ç9Hc               ƒ   óf  K  — d} t         rst         j                  «       \  }}t        j                  d|› �dd|› d�«       t        €t
        j                  d|«       ŒX	 t	        ||«      ƒ d{  –—†  | dz  } t         rŒs| S 7 Œ# t        $ r#}t
        j                  d	||d
¬«       Y d}~Œ2d}~ww xY w­w)zZRuns the webhook's booking for every order found paid at Stripe by `expire_unpaid_orders`.r   zstripe-unconfirmed:z-Stripe payment was never confirmed by webhookzpayment_intent=z„ succeeded at Stripe but the order was still unpaid. Check that the webhook endpoint is configured and STRIPE_WEBHOOK_SECRET is set.NuR   No payment reconciler registered â€” %s stays unpaid until the webhook is replayedr%   zBooking %s failed: %sT©Úexc_info)rv   Úpopr   Únotifyrx   r   Úerrorr   )Úbookedr•   r�   r   s       r   Úbook_orders_paid_at_striper    æ   sÃ   è ø€ à€FÝ
Ü'×+Ñ+Ó-‰ˆˆvÜ×ÑØ! % Ð)Ð+ZØ˜e˜Wð %Gð Hô	Iô Ð&Ü�I‰IÐjÐlqÔrØð	HÜ% e¨VÓ4×4Ð4Ø�a‰KˆFö ð €Mð	 5ùäò 	HÜ�I‰IÐ-¨u°aÀ$ˆI×GÑGûð	HüsH   ‚AB1ÁB Á-B Á.	B Á7B1Á>B1Â B Â	B.ÂB)Â$B1Â)B.Â.B1c                  ó¬  — t        t        t        d¬«      \  } }t        t        t	        «       d¬«      \  }}| |z   }|rt
        j                  d|«       |j                  «       D ]D  \  \  }}}	 ddlm	} |xs  ||«      }	t        j                  dd	||t	        ||	¬
«      dœd|	¬«       ŒF |r%t        j                  ddd|t	        «       dœd¬«       |t        j                  kD  r<t        j                   dd|› d�d|› dt        j                  › dt#        |«      › d�«       ddlm}
 dt(        fdt*        fdt,        fd|
j.                  ffD ]  \  }}	  |«        Œ 	 ddlm}  |«        |S # t        $ r |}	Y Œøw xY w# t        $ r!}t
        j1                  d||«       Y d }~ŒVd }~ww xY w# t        $ r!}t
        j7                  d|«       Y d }~|S d }~ww xY w)NT)r.   r%   )r/   z0GDPR: %d photo(s) deleted (per-school retention)r   )Úschool_id_for_familyzphoto.rgpd_deleterL   r2   )Úfiles_deletedÚretention_daysÚcritical)Úresource_typeÚresource_idÚdetailsÚseverityr   Úincoming)r¦   r§   r¨   r©   zrgpd-mass-deletezSuppression RGPD massive : z photosu%   Le nettoyage automatique a supprimÃ© z photos (seuil d'alerte u   ). Familles touchÃ©es : uH   . VÃ©rifiez le journal d'audit et la rÃ©tention (rgpd_days) des Ã©coles.)Úshoot_serviceu   aperÃ§us orphelinsÚ	corbeilleu   commandes impayÃ©eszshoot eventszCleanup '%s' failed: %s)Ú	purge_oldzRate-limit purge failed: %s)rO   r   r
   r   r    r   r�   ÚitemsÚservices.school_servicer¢   r   r   Ú	log_eventr   ÚRGPD_ALERT_THRESHOLDr   r�   r)   r   r«   rk   ru   r˜   Úprune_eventsrž   Úcore.rate_limitr­   r   )Úphotos_deletedrG   Úincoming_deletedÚ_rF   rK   rL   Únr¢   Úsidr«   r`   ry   r   r­   s                  r   Úrun_oncer¹   ú   sÿ  € Ü!,¬ZÔ9MÐY]Ô!^Ñ€N�JÜ%¤l´OÓ4EÐRSÔTÑÐ�aØÐ/Ñ/€GÙÜ�‰ÐCÀWÔMð  *×/Ñ/Ó1ò 

ÑÑˆ�˜!ð	ÝDØÒ8Ñ0°Ó8ˆCô 	×ÑØ¨xÀVØ&'¼?ÐW]ÐilÔ;mÑnØ¨3ö	
ð

ñ Ü×ÑØ¨zÀzØ&6Ì/ÓJ[Ñ\Ðgqõ	sð ”×,Ñ,Ò,Ü×ÑØÐ"=¸g¸YÀgÐ NØ3°G°9Ð<TÔU[×UpÑUpÐTqð r$Ü$'¨
£OÐ#4Ð4|ð~ô	õ
 'Ø*Ô,DÐEÈÔUbÐGcØ+Ô-AÐBÀ^ÐUb×UoÑUoÐDpðrò :‰ˆˆbð	:Ù�Dð:ð6Ý-ÙŒð €Nøô; ò 	ØŠCð	ûô, ò 	:Ü�I‰IÐ/°°q×9Ñ9ûð	:ûô
 ò 6Ü�‰Ð1°1×5Ð5Ø€Nûð6úsB   Á.E+ÅE<ÅF) Å+E9Å8E9Å<	F&ÆF!Æ!F&Æ)	GÆ2GÇGc               ƒ   ó†  K  — 	 	 t        j                  t        «      ƒ d {  –—†  t        «       ƒ d {  –—†  t        j                  t        «      ƒ d {  –—†  ŒV7 Œ87 Œ(# t         j                  $ r ‚ t
        $ rB} t        j                  d| d¬«       t        j                  ddt        | «      «       Y d } ~ Œd } ~ ww xY w7 Œj­w)NTzErreur cleanup RGPD: %srš   zcleanup-failedu   Le nettoyage RGPD a Ã©chouÃ©)ÚasyncioÚ	to_threadr¹   r    ÚCancelledErrorr   r   rž   r   r�   ÚstrÚsleepÚ_CLEANUP_INTERVAL©r   s    r   Úrun_cleanup_looprÂ   '  s©   è ø€ Ø
ð	[Ü×#Ñ#¤HÓ-×-Ð-Ü,Ó.×.Ð.ô �m‰mÔ-Ó.×.Ð.ð à-øØ.ùÜ×%Ñ%ò 	ØÜò 	[Ü�I‰IÐ/°¸TˆIÔBÜ× Ñ Ð!1Ð3QÔSVÐWXÓSY×ZÑZûð	[úð 	/úsY   ‚C…A ¡A¢A ³A´A ¸CÁB?ÁCÁA ÁA ÁB<Á:8B7Â2CÂ7B<Â<Cr#   )Fr   )r   N)5Ú__doc__r»   ÚloggingÚrer6   Údatetimer   Úpathlibr   Útypingr   Úcorer   Úcore.configr   r   r	   r
   r   Úcore.dbr   Úcore.timeutilr   r   r   r   Ú	getLoggerr   rÀ   r:   r;   Úcompiler^   r¾   r   r    Útupler*   rb   ÚdictrO   rY   rk   ru   rx   rv   ÚlistÚ__annotations__rz   r˜   r    r¹   rÂ   rÁ   s   0r   ú<module>rÓ      sz  ðòó" Û Û 	Û Ý Ý Ý å ß dÕ dÝ Ý  ß 1à€g×ÑÐ*Ó+€àÐ Ø 0Ö1˜1ˆQ�W‰W�YÒ1€	Øˆb�j‰jÐZÓ[€ñ
  ¨#¡ð 
 À(È3Á-ð 
 Ð[^ó 
 ð�ð ˜% ¨¡¨x¸©}Ð <Ñ=ó ñ"&˜4ð &¨sð &¸tð &ÐY\ð &ÐejÐknÐptÐktÑeuó &ðR ð ¨Sð °Tó ð+ #ó +ð\	�só 	ð Ð à)+€��e˜C ˜H‘oÑ&Ó +óð
)˜có )ðX¨#ó ð(*�#ó *ôZ
/ùòI 2s   Á&D