ó
    +ûËj2  ã                   ó<  • % S r SSKrSSKrSSKrSSKrSSKJr  SSKJr  SSK	J
r
  SSKJr  SSKJrJrJrJrJr  SSKJr  SS	KJr  SS
KJrJr  \R4                  " S5      rSr\ V s1 s H  o R;                  5       iM     sn r\R>                  " S5      r S#S\
\!   S\
\!   S\"4S jjr#S\S\$\
\!   \
\!   4   4S jr%S$S\S\"S\&S\$\"\'4   4S jjr(S\S\!S\&4S jr)S\"4S jr*S\"4S jr+Sq,/ r-\.\$\!\"4      \/S'   S%S jr0S\"4S jr1S\"4S  jr2S\"4S! jr3S%S" jr4gs  sn f )&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 (filigrane / 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 z5^(?P<stem>.+?)\.(?:wm|h[0-9a-f]{10})(?:\.\d+)?\.webp$Úfamily_codeÚ	school_idÚreturnc                 óü   •  SSK Jn  U(       a  [        UR                  U5      S   5      $ U (       a  UR	                  U 5      $  [        $ ! [
         a&  n[        R                  SXU5         SnA[        $ SnAff = f)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       Ú[D:\Ingenieria\TRABAJOS ONLINE\Projects\11- Photo School\backend\services\cleanup_service.pyÚ_retention_daysr    '   sy   € ð^Ý+ÞÜ�~×2Ñ2°9Ó=¸kÑJÓKÐKÞØ!×;Ñ;¸KÓHÐHð ô  Ðøô ó ^Ü�‰ÐAÀ9Ð[\×]Ð]ÜÐûð^ús   ‚)A ¬A Á
A;ÁA6Á6A;Úrelc                 óÞ   • U R                   nU(       d  gUS   S:X  a1  [        U5      S:¼  a
  US   US   4$ [        U5      S:”  a  US   OSS4$ [        U5      S:¼  a
  US   US   4$ SUS   4$ )	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_familyc                 ól  • U R                  5       (       d  S0 4$ [        R                  " 5       nSn0 n0 nU R                  S5       Há  nUR                  5       (       a"  UR                  R                  5       [        ;   d  M<  SnU(       a3  [        UR                  U 5      5      nX†;  a  Uu  pš[        X©S9Xh'   Xh   nOUn UR                  5       R                  X;S-  -
  :  aC  UR                  5         US-  nU(       a%  US   (       a  UR                  US5      S-   XX'   MÝ  Mß  Má  Mã     [#        U R                  S5      SS	9 H+  nUR%                  5       (       d  M   UR'                  5         M-     XE4$ ! [         a"  n[        R!                  SX|5         SnAGMW  SnAff = f! [         a     Mn  f = f)
u�   Supprime les images trop vieilles. Si `by_family`, la rÃ©tention est celle de l'Ã©cole de chaque famille.
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_dirÚrmdir)r,   r-   r.   ÚnowÚdeletedÚ
per_familyÚcacheÚfÚkeyÚschoolÚfamilyÚdaysr   Úds                 r   Ú_delete_oldrN   E   s~  € ð ×Ñ×ÑØ�"ˆuˆÜ
�)Š)‹+€CØ€GØ€JØ€EØ�_‰_˜SÖ!ˆØ—	‘	—‘ §¡§¡Ó 0´IÓ =ÙØˆÞÜ˜!Ÿ-™-¨	Ó2Ó3ˆCØÓØ!$‘�Ü,¸ÑR�‘
Ø‘:‰DàˆDð	?Ø�v‰v‹x× Ñ  3°©Ñ#5Ó5Ø—‘”
Ø˜1‘�Þ˜3˜qŸ6Ø&0§n¡n°S¸!Ó&<¸qÑ&@�J“Oñ "‘3ñ 6ñ "ô( �I—O‘O CÓ(°$Ô7ˆØ�8‰8�:‹:ðØ—‘–	ñ 8ð ÐÐøô ó 	?Ü�K‰KÐ7¸×>Ó>ûð	?ûô ó Úðús+   ÃA E6ÅF%Å6
F"Æ FÆF"Æ%
F3Æ2F3Ú
photos_dirÚstemc                 ól   ^•  [        U4S jU R                  5        5       5      $ ! [         a     gf = f)Nc              3   ón   >#   • U  H*  oR                  5       (       d  M  UR                  T:H  v •  M,     g 7f©N)r7   rP   )Ú.0rH   rP   s     €r   Ú	<genexpr>Ú#_original_exists.<locals>.<genexpr>m   s#   øé € ÐOÒ+? aÇ9Á9Ç;“>�1—6‘6˜T–>Ò+?ùs   ƒ5Ÿ5F)ÚanyÚiterdirr@   )rO   rP   s    `r   Ú_original_existsrY   k   s4   ø€ ðÜÔO¨:×+=Ñ+=Ô+?ÓOÓOÐOøÜó Ùðús   ƒ"& ¦
3²3c                  ó¤  • Sn [         R                  " 5       (       Ga  [         R                  " S5       H¬  nUR                  5       (       d  M  [        R                  UR                  5      nU(       a  UR                  S5      OUR                  n [        UR                  R                  [         5      -  n[        XC5      (       a  M–   UR                  5         U S-  n M®     [!        [         R                  " S5      SS9 H+  nUR#                  5       (       d  M   UR%                  5         M-     [        R                  " 5       (       a­  [        R                  " S5       H“  nUR#                  5       (       d  M  UR'                  5        HT  nUR                  R)                  S5      S   n[        UR                  U5      (       a  M>   UR                  5         U S-  n MV      UR%                  5         M•     U $ ! [         a     GMÔ  f = f! [         a     GMæ  f = f! [         a     GM*  f = f! [         a     M²  f = f! [         a     Mí  f = f)	uc   Supprime aperÃ§us et cache `.protect/` dont l'original n'existe plus. Retourne le nombre supprimÃ©.r   r0   rP   r%   Tr2   z.protectÚ_heads_)r	   r4   r6   r7   Ú_PREVIEW_REÚmatchÚnameÚgrouprP   r   Úparentr;   Ú
ValueErrorrY   r>   r@   rA   rB   rC   rX   Úsplit)ÚremovedrH   ÚmrP   Ú	originalsrM   Úprotects          r   Úprune_orphan_derivativesrg   r   sÖ  € à€GÜ×Ò×ÒÜ×!Ò! #Ö&ˆAØ—9‘9—;‘;ÙÜ×!Ñ! !§&¡&Ó)ˆAÞ&'�1—7‘7˜6”?¨Q¯V©VˆDðÜ&¨¯©×)=Ñ)=¼jÓ)IÑI�	ô $ I×4Ó4ðØ—H‘H”JØ˜q‘L’Gñ 'ô œ
×(Ò(¨Ó-°tÔ<ˆAØ�x‰x�z‹zðØ—G‘G–Iñ =ô ×Ò×ÑÜ!×'Ò'¨
Ö3ˆGØ—>‘>×#Ñ#ÙØ—_‘_Ö&�Ø—v‘v—|‘| IÓ.¨qÑ1�Ü'¨¯©¸×=Ó=ðØŸ™œ
Ø 1™šñ 'ðØ—‘–ñ 4ð €Nøô= ó Ûðûô ó Ûðûô ó Ûðûô #ó Úðûô ó ÚðúsZ   Â&G:ÃHÄHÇ
H0Ç$IÇ:
H	ÈH	È
HÈHÈ
H-È,H-È0
H>È=H>É
IÉIc                  ó  • SSK Jn Jn  Sn UR                  5        H>  n[	        [
        R                  [        US   S95      nX R                  US   U5      -  nM@     U$ ! [         a!  n[        R                  SU5         S nAU$ S nAff = f)Nr   )Úphoto_servicer   Úid)r   zTrash purge failed: %s)r   ri   r   Úlist_schoolsÚminr   ÚTRASH_RETENTION_DAYSr    Úpurge_trashr   r   r   )ri   r   ÚpurgedÚsrL   r   s         r   Úpurge_trashesrq   ž   s‡   € ß6Ø€Fð1Ø×,Ñ,Ö.ˆAÜ”v×2Ñ2´OÈaÐPTÉgÑ4VÓWˆDØ×/Ñ/°°$±¸Ó>Ñ>ŠFñ /ð
 €Møô ó 1Ü�‰Ð,¨a×0Ð0Ø€Mûð1ús   ŒAA  Á 
BÁ*BÂBÚ_paid_at_stripec                 ó   • U q g rS   )Ú_payment_reconciler)Úfns    r   Úset_payment_reconcilerrv   °   s   € àÑr+   c            	      óR  • [        5       [        [        R                  S9-
  R	                  5       n [        5        nUR                  SU 45      R                  5       nSSS5        Sn[        R                  (       a  SSK	n[        R                  Ul
        / nW HÆ  nUS   nU(       a£  Ub    UR                  R                  U5      nUS   S:X  aW  [        R                  U[        UR!                  S5      =(       d    US	   5      45        ["        R%                  S
US   U5        MŽ  US   S:w  a  UR                  R'                  U5        UR                  US   5        MÈ     U(       a^  [        5        nU H  n	UR                  SU	45        M     UR+                  5         SSS5        ["        R-                  S[/        U5      5        [/        U5      $ ! , (       d  f       GN}= f! [(         a&  n["        R%                  SUS   Xh5         SnAGMw  SnAff = f! , (       d  f       N}= f)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 expiringrj   Ú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Úretrieverr   Ú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Ü	Œ�TØ�|‰|ðbØdjÐclónçnvÑnvÓnxð 	÷ 
ð
 €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˜Û%�Ø—‘ÐhÐksÐjuÖvñ &à�K‰KŒM÷ ô 	�‰Ð-¬s°9«~Ô>Üˆy‹>Ð÷C 
Žûô, ó Ü—‘Ð_ÐabÐcgÑahÐjoÔsÞûðú÷ �Xús6   »"GÂ,A9G%Ä'$G%Å4-HÇ
G"Ç%
HÇ/HÈHÈ
H&c               ƒ   óx  #   • Sn [         (       az  [         R                  5       u  p[        R                  " SU 3SSU S35        [        c  [
        R                  SU5        M_   [	        X5      I Sh  v•N   U S-  n [         (       a  Mz  U $  N! [         a  n[
        R                  S	XS
S9   SnAN5SnAff = f7f)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)rr   Úpopr   Únotifyrt   r   Úerrorr   )Úbookedr‘   r}   r   s       r   Úbook_orders_paid_at_striperœ   á   s½   é € à€Fß
Š/Ü'×+Ñ+Ó-‰ˆÜ×ÒØ! % Ð)Ð+ZØ˜e˜Wð %Gð Hô	Iô Ñ&Ü�I‰IÐjÐlqÔrÙð	HÜ% eÓ4×4Ð4Ø�a‰KˆF÷ Œ/ð €Mñ	 5øäó 	HÜ�I‰IÐ-¨uÀ$ˆIÖGûð	HüsH   ‚A"B:Á%B Á3BÁ4	B Á=B:Â
B:ÂB Â
B7ÂB2Â-B:Â2B7Â7B:c                  ó’  • [        [        [        SS9u  p[        [        [	        5       5      u  p#X-   nU(       a  [
        R                  SU5        UR                  5        HC  u  u  pVn SSKJ	n  U=(       d    U" U5      n	[        R                  " SSUU[	        XiS9S	.S
U	S9  ME     U(       a#  [        R                  " SSSU[	        5       S	.S
S9  U[        R                  :”  a=  [        R                   " SSU S3SU S[        R                   S[#        U5       S35        S[$        4S[&        4S[(        44 H  u  p« U" 5         M      SSKJn  U" 5         U$ ! [         a    Un	 Nèf = f! [         a!  n[
        R+                  SX¬5         S nAMY  S nAff = f! [         a!  n[
        R1                  SU5         S nAU$ S nAff = f)NT)r.   z0GDPR: %d photo(s) deleted (per-school retention)r   )Úschool_id_for_familyzphoto.rgpd_deleterK   r1   )Ú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.u   aperÃ§us orphelinsÚ	corbeilleu   commandes impayÃ©eszCleanup '%s' failed: %s)Ú	purge_oldzRate-limit purge failed: %s)rN   r   r
   r   r    r   r‹   ÚitemsÚservices.school_servicerž   r   r   Ú	log_eventr   ÚRGPD_ALERT_THRESHOLDr   r™   r)   rg   rq   r”   rš   Úcore.rate_limitr¨   r   )Úphotos_deletedrF   Úincoming_deletedÚ_rE   rJ   rK   Únrž   Úsidr^   ru   r   r¨   s                 r   Úrun_oncer³   õ   sÒ  € Ü!,¬ZÔ9MÐY]Ñ!^Ñ€NÜ%¤l´OÓ4EÓFÑÐØÑ/€GÞÜ�‰ÐCÀWÔMð  *×/Ñ/Ö1ÑÑˆ˜!ð	ÝDØ×8Ñ0°Ó8ˆCô 	×ÒØ¨xÀVØ&'¼?ÐW]Ñ;mÑnØ¨3ô	
ñ  2ö Ü×ÒØ¨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óD‰ˆð	:ÙŽDñDð6Ý-ÙŒð €Nøô9 ó 	ØŠCð	ûô* ó 	:Ü�I‰IÐ/°×9Ò9ûð	:ûô
 ó 6Ü�‰Ð1°1×5Ð5Ø€Nûð6úsB   Á*EÅ E-ÅF ÅE*Å)E*Å-
FÅ7FÆFÆ
GÆ%GÇGc               ƒ   óŒ  #   •   [         R                  " [        5      I S h  v•N   [        5       I S h  v•N   [         R                  " [        5      I S h  v•N   MY   N: N*! [         R                   a    e [
         a@  n [        R                  SU SS9  [        R                  " SS[        U 5      5         S n A N�S n A ff = f Nk7f)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ÁCÁCÁA! ÁA! Á!B?Á?6B:Â5CÂ:B?Â?Cr#   )F)r   N)5Ú__doc__rµ   ÚloggingÚrer5   Údatetimer   Úpathlibr   Útypingr   Úcorer   Úcore.configr   r   r	   r
   r   Úcore.dbr   Úcore.timeutilr   r   r   r   Ú	getLoggerr   rº   r9   r:   Úcompiler\   r¸   r   r    Útupler*   ÚboolÚdictrN   rY   rg   rq   rt   rr   ÚlistÚ__annotations__rv   r”   rœ   r³   r¼   r»   s   0r   Ú<module>rÎ      sr  ðòó" Û Û 	Û Ý Ý Ý å ß dÕ dÝ Ý  ß 1à×ÒÐ*Ó+€àÐ Ù 0Ó1Ò 0˜1�W‰WŽYÑ 0Ñ1€	Ø�jŠjÐQÓR€ñ
  ¨#¡ð 
 À(È3Á-ð 
 Ð[^õ 
 ð�ð ˜% ¨¡¨x¸©}Ð <Ñ=ô ñ"#˜4ð #¨sð #¸tð #ÐPUÐVYÐ[_ÐV_ÑP`õ #ðL ð ¨Sð °Tô ð) #ô )ðX	�sô 	ð Ð à)+€��e˜C ˜H‘oÑ&Ó +ôð
)˜cô )ðX¨#ô ð()�#ô )õX
/ùò} 2s   Á&D