
    ]YjU                       U d Z ddlm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	m
Z
m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  eej,                  j/                  dd	            Zej3                  d
d
       edz  Zedz  Zej,                  j/                  dd      ZdZ ed      Zg dZdgZ g dZ! ede"      Z#d7dZ$d7dZ%d8dZ&d9dZ'd:dZ(d;dZ)e#jU                  ddg      d         Z+e#jU                  d!dd"g      d#        Z,e#jU                  d!d$g      d%        Z-d<d&Z.d=d'Z/d>d(Z0d>d)Z1d?d*Z2d@d+Z3d8d,Z4dAd-Z5	 	 dB	 	 	 	 	 	 	 	 	 dCd.Z6dDd/Z7dEd0Z8dd1d2Z9de:d3<   d4Z;	 dF	 	 	 	 	 	 	 dGd5Z<	 	 	 	 	 	 	 	 dHd6Z=y)Ia  
Modulo de rastreamento PX3 Lab.

- Endpoint /track/wa: registra clique em link de WhatsApp (com UTMs) e redireciona pro wa.me.
- Endpoint /track/lp: beacon pra registrar pageview da landing (chamado por GTM/JS).
- Helpers de agregacao usados pela aba /rastreamento do dashboard.
- Helpers de atribuicao real cruzando contatos GHL (tags) com gasto Meta/Google.

Os registros de clique/pageview sao persistidos em JSONL (uma linha por evento), pra ler sem
dependencia de banco. Arquivos em /opt/mia/workspace/clientes/px3lab/tracking/.
    )annotationsN)datetime	timedeltatimezone)Path)Iterable)	BlueprintrequestredirectjsonifyPX3_TRACKING_DIRz+/opt/mia/workspace/clientes/px3lab/trackingT)parentsexist_okzclicks.jsonlzpageviews.jsonlPX3_WA_PHONE5511934313883)
utm_source
utm_mediumutm_campaignutm_contentutm_termz/tmp/px3_tags_linkia.json)zcomprou-claud-photorfzcomprou-checkout-photorfzcomprou-e-photorfzcomprou-fast-albumzcomprou-photo-drivenutricao_cloud_photorf)
iniciou_fluxo_compra_cloud$iniciou_fluxo_compra_checkoutphotorfiniciou_fluxo_compra_ephotorfiniciou_fluxo_compra_fastalbuminiciou_fluxo_compra_photodriveiniciou_fluxo_trial_cloud#iniciou_fluxo_trial_checkoutphotorfiniciou_fluxo_trial_ephotorfiniciou_fluxo_trial_fastalbuminiciou_fluxo_trial_photodrivetrackingc                 h    t        j                  t        j                        j	                  d      S )Nsecondstimespec)r   nowr   utc	isoformat     8/opt/mia/workspace/clientes/px3lab/dashboard/tracking.py_now_isor-   V   s#    <<%///CCr+   c                     t         j                  j                  dd      } | r"| j                  d      d   j	                         S t         j                  j                  d      xs t         j
                  xs dS )z<Pega o IP real do cliente respeitando proxy (Traefik/Nginx).zX-Forwarded-For ,r   z	X-Real-IP)r
   headersgetsplitstripremote_addr)fwds    r,   
_client_ipr7   Z   s]    
//

/
4C
yy~a &&((??{+Hw/B/BHbHr+   dictc                     i } t         D ];  }t        j                  j                  |      xs dj	                         }|s7|| |<   = | S )zColeta UTMs da querystring.r/   )
UTM_FIELDSr
   argsr2   r4   )outkvs      r,   _collect_utmsr?   b   sK    
C \\a &B--/CF Jr+   c                    	 | j                  dd      5 }|j                  t        j                  |d      dz          ddd       y# 1 sw Y   yxY w# t        $ r Y yw xY w)z*Append seguro (uma linha JSON por evento).autf-8encodingF)ensure_ascii
N)openwritejsondumps	Exception)pathrecordfs      r,   _append_jsonlrO   l   s_    YYsWY- 	CGGDJJvE:TAB	C 	C 	C s-   A *AA AA A 	A A c                T    | st         S t        j                  dd|       }|xs t         S )Nz\Dr/   )DEFAULT_WA_PHONEresub)rawdigitss     r,   _clean_phonerV   v   s)    VVE2s#F%%%r+   c                \    d|  }|r$| dt         j                  j                  |       S |S )Nzhttps://wa.me/z?text=)urllibparsequote)phonetextbases      r,   _build_wa_urlr^   }   s7    E7#Dvfll006788Kr+   z	/track/waGET)methodsc                    t               } t        t        j                  j	                  d            }t        j                  j	                  d      xs dj                         }t               t               t        j                  j	                  dd      t        j                  j	                  dd      |d| }t        t        |       t        t        ||      d      S )	z5
    Registra clique e redireciona pro WhatsApp.
    r[   r\   r/   
User-AgentReferer)tsip
user_agentrefererr[   i.  )code)r?   rV   r
   r;   r2   r4   r-   r7   r1   rO   CLICKS_FILEr   r^   )utmsr[   r\   rM   s       r,   track_wark      s    
 ?D))'23ELLV$*113D jloo)),;??&&y"5 F +v&M%.S99r+   z	/track/lpPOSTc                 \   t               } i }t        j                  r	 t        j                  d      xs i }t        D ]E  }| j                  |      r|j                  |      s't        ||         j                         | |<   G dD ](  }|j                  |      st        ||         dd ||<   * 	 dD ]C  }t        j                  j                  |      xs dj                         }|s7||vs<|dd ||<   E t               t               t        j                  j                  dd      t        j                  j                  dd      d	| |}t        t        |       t        d
di      }d|j                  d<   d|j                  d<   d|j                  d<   |S # t        $ r Y w xY w)zA
    Beacon de pageview da landing (GTM/JS chama via fetch).
    T)silent)pagetitlerL   eventNi,  r/   rb   rc   )rd   re   rf   rg   ok*Access-Control-Allow-OriginGET, POST, OPTIONSAccess-Control-Allow-MethodsContent-TypeAccess-Control-Allow-Headers)r?   r
   is_jsonget_jsonr:   r2   strr4   rK   r;   r-   r7   r1   rO   PAGEVIEWS_FILEr   )rj   extrabodyr=   r>   rM   resps          r,   track_lpr      s   
 ?DE		##406BD 3xx{txx{!$q'l002DG3 8 288A;"47|DS1E!H2 0 \\a &B--/%#wE!H jloo)),;??&&y"5	
  F .&)D$< D25DLL./3GDLL/03ADLL/0K-  		s#   4F F %7F F 	F+*F+OPTIONSc                 z    t        ddi      } d| j                  d<   d| j                  d<   d| j                  d<   | S )	Nrr   Trs   rt   ru   rv   rw   rx   )r   r1   )r   s    r,   track_lp_optionsr      sB    D$< D25DLL./3GDLL/03ADLL/0Kr+   c              #  "  K   | j                         sy 	 | j                  dd      5 }|D ]-  }|j                         }|s	 t        j                  |       / 	 d d d        y # t
        $ r Y Ew xY w# 1 sw Y   y xY w# t
        $ r Y y w xY ww)NrrB   rC   )existsrG   r4   rI   loadsrK   )rL   rN   lines      r,   _iter_jsonlr      s     ;;=YYsWY- 	 zz|**T**	 	 ! 	 	  so   BB  A4A%A4B  $B%	A1.A40A11A44A=9B  <B=B   	B	BBBc                r    | sy 	 t        j                  | j                  dd            S # t        $ r Y y w xY wNZz+00:00)r   fromisoformatreplacerK   )rd   s    r,   	_parse_tsr      s;    %%bjjh&?@@ s   $* 	66c                 2    t        t        t                    S N)listr   ri   r*   r+   r,   carregar_clicksr      s    K())r+   c                 2    t        t        t                    S r   )r   r   r|   r*   r+   r,   carregar_pageviewsr      s    N+,,r+   c                   t        j                  t        j                        }|t	        d      z
  }|t	        d      z
  }d}d}d}i }i }| D ]  }	|dz  }t        |	j                  dd            }
|
r|
|k\  r|dz  }|
|k\  r|dz  }|	j                  d	      xs d
j                         j                         }|	j                  d      xs dj                         j                         }| d| }|j                  |d      dz   ||<   |	j                  d      xs dj                         j                         }|j                  |d      dz   ||<    dd}||| ||d       ||d      dS )N   hours   daysr      rd   r/   r   diretor   -z / r   zsem-campanhac                    | sg S t        | j                               xs d}| j                         D cg c]  \  }}||d|d||z  dz  i }}}|j                  d        |S c c}}w )Nr   qtdpctd   c                    | d    S )Nr   r*   )xs    r,   <lambda>z2agregar_clicks.<locals>._ranking.<locals>.<lambda>  s    5	 r+   key)sumvaluesitemssort)dkey_namesomar=   r>   rowss         r,   _rankingz agregar_clicks.<locals>._ranking  sw    I188:#! 	
1 q%E1t8c>:
 
 			)	*
s   A%canalcampanha)totalultimas_24h
ultimos_7d	por_canalpor_campanha)r   zdict[str, int]r   r{   return
list[dict])	r   r'   r   r(   r   r   r2   r4   lower)clicksagorah24d7r   last_24last_7r   r   crd   srcmedr   campr   s                   r,   agregar_clicksr      s}   LL&E
)"%
%C	"	"BEGF "I#%L ;
quuT2'Sy1Rx!uu\".h557==?uu\")c00288:%s3% $==2Q6	%n%7>>@FFH)--dA6:T;"	 i1 z: r+   c                   t        j                  t        j                        }|t	        d      z
  }|t	        d      z
  }d}d}d}| D ]:  }|dz  }t        |j                  dd            }|s&||k\  r|dz  }||k\  s6|dz  }< |||d	S )
Nr   r   r   r   r   r   rd   r/   )r   r   r   )r   r'   r   r(   r   r   r2   )	pvsr   r   r   r   r   r   prd   s	            r,   agregar_pageviewsr   %  s    LL&E
)"%
%C	"	"BEGF 
quuT2'Sy1Rx!  r+   c                    t         j                         r	 t        j                  t         j	                  d            } | j                  dg       D cg c]  }|d   	 c}xs t        t              | j                  dg       D cg c]  }|d   	 c}xs t        t              | j                  dg       D cg c]  }|d   	 c}xs t        t              | j                  dg       D ci c]  }|d   |j                  dd       c}d	d
S t        t              t        t              t        t              i dd
S c c}w c c}w c c}w c c}w # t        $ r Y Nw xY w)aE  
    Le /tmp/px3_tags_linkia.json (mantido pela Amanda) com estrutura:
        {
          "compra":   [{"nome": "...", "id": "...", "produto": "..."}],
          "nutricao": [{"nome": "...", "id": "..."}],
          "iniciou_fluxo": [{"nome": "...", "id": "..."}]
        }
    Se nao existir, retorna default embutido.
    rB   rC   compranomenutricaoiniciou_fluxoprodutor/   file)r   r   r   compra_produtossourcedefault)	TAGS_FILEr   rI   r   	read_textr2   r   TAGS_COMPRA_DEFAULTTAGS_NUTRICAO_DEFAULTTAGS_INICIOU_FLUXO_DEFAULTrK   )datats     r,   carregar_tags_configr   >  s6    
	::i1171CDD.2hhx.DE1V9EbNaIb04R0HI1QvYIhTRgMh59XXor5R!S!F)!S!wW[\vWwMQXXV^`bMc#dAfIquuY/C$C#d   *+./89  FI!S#d  		sG   =E D>'E E'E 9E'E ,E
	E >E 	EEc                   | syt        | d       }|D ]  }|j                  d      xs dj                         }|j                  d      xs |j                  d      xs dj                         }|j                  d      xs |j                  d	      xs dj                         }d
|v sd|v sd|v r yd|v sd|v r y|dv s|dk(  r y|dv r y|dv r yd|v r	|r|dk7  r y|j                  d      s|dk(  s y y)z
    Classifica o canal do lead com base nas atribuicoes do GHL.

    Retorna: 'meta' | 'google' | 'whatsapp' | 'crm' | 'direto'
    r   c                ,    | j                  d      rdS dS )NisFirstr   r   )r2   )rA   s    r,   r   z$_classificar_canal.<locals>.<lambda>f  s    aeeI6F A r+   r   	utmSourcer/   	utmMediummediumutmSessionSourcesessionSourcefacebookmeta	instagramgoogleadwords)r   r   r   fb)r   r   )whatsappwhatsapp_coexwar   zsocial mediamanualcrm)sortedr2   r   
startswith)attributionsorderedattusumsss         r,   _classificar_canalr   ]  s    \'MNG ggk"(b//1ggk"=cggh&7=2DDFgg()KSWW_-EKRRT v|{b/@r>Y"_ 22bDj&&44 RB2>==2>14 r+   c                   d}|dd}g }t        |      D ]"  }d}t        d      D ]l  }		 t        j                  || |d      }
|
j                  d	k(  rt	        j
                  d
|	d
z  z          H|
j                  dk7  rd} n	 |
j                         } n |s |S |j                  dg       xs g }|s |S |j                  |       |j                  di       xs i }|j                  d      }|j                  d      }|s|j                  d      s |S |s |S ||d<   ||d<   t	        j
                  |       % |S # t        $ r t	        j
                  d|	dz  z          Y >w xY w# t        $ r d}Y w xY w)a  
    Paginacao completa via GET /contacts/?locationId=...
    Cada pagina retorna ate 100. Ate `limit_pages` paginas => ~100*limit_pages contatos.

    Faz retry inteligente em 429 (rate limit). Se um erro persistir, retorna o que
    ja conseguiu carregar (nao explode).
    z.https://services.leadconnectorhq.com/contacts/r   )
locationIdlimitN   -   )r1   paramstimeout   i        contactsr   
startAfterstartAfterIdnextPageUrl)	rangerequestsr2   rK   timesleepstatus_coderI   extend)r1   location_idlimit_pagespausaurlr   contatospage_ir   	tentativar   batchr   
next_startnext_ids                  r,   buscar_todos_contatos_ghlr    s    ;C'#6FH$ (q 	ILLgfbQ }}#

1y1},-}}#FFH #	& " O j"%+ O 	uuVR &BXXl+
((>*$((="9 O  O  *|!(~

5Q(R OG  

1y1},-  s#   D=E(=$E%$E%(E65E6c                v    | sy | j                  dd      }	 t        j                  |      S # t        $ r Y y w xY wr   )r   r   r   rK   )r>   ss     r,   	_parse_dtr    s@    			#x A%%a(( s   , 	88c           
        g d}|D ci c]  }|ddddg d c}i }d}d}d}g }t        d |j                  dg       D              t        d |j                  dg       D              t        d |j                  d	g       D              | D ]  }|j                  d
      xs g D 	cg c]  }	t        |	      j                          }
}	t	        |j                  d            }|vrd}|   dxx   dz  cc<   t        |j                  d      xs d      }|rV|j                  t        j                        j                  d      }||vr|D ci c]  }|d c}||<   ||   |xx   dz  cc<   t        fd|
D              }t        fd|
D              }t        fd|
D              }|r|   dxx   dz  cc<   |dz  }|r|   d	xx   dz  cc<   |dz  }|sQ|   dxx   dz  cc<   |dz  }t        |j                  d      xs d      }|s|s||k\  s||z
  j                         dz  }|   d   j                  |       |j                  |        d-d}d.d}j                         D ]  \  }}|d   }|r!t        t        |      t!        |      z  d      nd|d<   |rt         ||      d      nd|d<    ||      |d<   |d   rt        |d   |d   z  d z  d!      nd|d"<   t!        |      |d#<   |d=  g }t#        |j%                               D ]  }d$|i||   }|j                  |        t        fd%|D              }||||||d&|r!t        t        |      t!        |      z  d      nd|rt         ||      d      nd ||      t!        |      d'|j                  d(d)      t'        j(                  t        j                        j+                  d*+      d,S c c}w c c}	w c c}w )/a<  
    Recebe lista de contatos GHL crus e retorna dicionario com:

    - por_canal: {'meta': {...}, 'google': {...}, 'whatsapp': {...}, 'crm': {...}, 'direto': {...}}
        cada valor contem: leads, compras, taxa_conv, tempos_dias (list),
        tempo_medio_dias, tempo_mediano_dias, distrib_faixas
    - serie_diaria: [{data:'YYYY-MM-DD', meta:X, google:Y, whatsapp:Z, crm:A, direto:B}]
    - total: {'leads': N, 'compras': N, 'com_nutricao': N, 'iniciou_fluxo': N}
    - tempos_globais: {'media': X, 'mediana': X, 'faixas': {'<7':N, '7-30':N, '30-90':N, '>90':N}}
    )r   r   r   r   r   r   )leadscomprascom_nutricaor   tempos_diasc              3  <   K   | ]  }|j                           y wr   r   .0r   s     r,   	<genexpr>z&analisar_atribuicao.<locals>.<genexpr>  s     DAaggiD   r   c              3  <   K   | ]  }|j                           y wr   r!  r"  s     r,   r$  z&analisar_atribuicao.<locals>.<genexpr>  s     Ha	Hr%  r   c              3  <   K   | ]  }|j                           y wr   r!  r"  s     r,   r$  z&analisar_atribuicao.<locals>.<genexpr>  s     LQqwwyLr%  r   tagsr   r   r  r   	dateAddedr/   z%Y-%m-%dc              3  &   K   | ]  }|v  
 y wr   r*   )r#  r   tags_nutricaos     r,   r$  z&analisar_atribuicao.<locals>.<genexpr>  s     <!1-<   c              3  &   K   | ]  }|v  
 y wr   r*   )r#  r   tags_inicious     r,   r$  z&analisar_atribuicao.<locals>.<genexpr>  s     :!|+:r,  c              3  &   K   | ]  }|v  
 y wr   r*   )r#  r   tags_compras     r,   r$  z&analisar_atribuicao.<locals>.<genexpr>   s     51a;&5r,  r  r  dateUpdatedg     @r  c                    ddddd}| D ]H  }|dk  r|dxx   dz  cc<   |dk  r|dxx   dz  cc<   )|dk  r|d	xx   dz  cc<   <|d
xx   dz  cc<   J |S )Nr   )<77-3030-90>90r   r3  r      r4  Z   r5  r6  r*   )temposbr   s      r,   _faixasz$analisar_atribuicao.<locals>._faixas  so    a!A6 	A1u$1R&	Q	R'
a
%A	 r+   c                |    | syt        |       }t        |      }|dz  r||dz     S ||dz  dz
     ||dz     z   dz  S )N        r   r   g       @)r   len)valsr  ns      r,   _medianaz%analisar_atribuicao.<locals>._mediana!  sS    4LFq5Q!V9!q&1*!q&	)S00r+   r=  tempo_medio_diastempo_mediano_diasdistrib_faixasr   r   	taxa_convamostra_temposr   c              3  .   K   | ]  }|   d      yw)r  Nr*   )r#  r   r   s     r,   r$  z&analisar_atribuicao.<locals>.<genexpr>:  s     <il7+<s   )r  r  r  r   )mediamedianafaixasamostrar   r   r$   r%   )r   canaisserie_diariar   tempos_globaistags_source	gerado_em)r9  list[float]r   r8   )r?  rQ  r   float)setr2   r{   r   r   r  
astimezoner   r(   strftimeanytotal_secondsappendr   roundr   r>  r   keysr   r'   r)   )r  tags_cfgrL  r   	serie_maptotal_com_nutricaototal_iniciou_fluxototal_comprasrN  r   r(  r   dt_adddata_isor=   tem_nutricaotem_inicioucomproudt_upddiasr;  rA  dadosr9  rM  rowtotal_leadsr   r0  r.  r+  s                              @@@@r,   analisar_atribuicaorj    sT    =F 	"  	

 	
	"I ,.IM"$NDh)CDDKH8<<
B+GHHML(,,*KLLL #,)*v)<">1A>>"155#89	!E%!Q&! 155-34((6??
KHy(5;&<q!t&<	(#h&!+&<t<<:T::555e^,1,!#e_-2-1$eY'1,'QM quu]39r:F&Vv%56687B% /66t<%%d+G#,L1 ") !u}%KQE#f+F*CQ$GWZ !DJeHV,<a&@PS"#")&/RWX_R`U5#3eGn#Ds#JANfik"%f+- ! L9>>+, !x79X#67C ! <V<<K $ $.0	
 M[U3~.^1DDaH`c=KuXn5q9QTn->*	
  ||Hi8\\(,,/9999M# I	". ? '=s   O3- O8
O=r=  )r   rd   _ATRIB_CACHEi  c                .   t        j                          }|s%t        d   r|t        d   z
  t        k  r	t        d   S t               }t	        | |d      }t        ||      }t        |      |d<   t        j                  d      }|r~|j                  dd      t        |      dz  kD  r]t        |      dk  rOt        j                  t        j                        j                  d	
      |d<   t        |      |d<   |t        d<   |S |t        d<   |t        d<   |S )zCache de 30 min para nao esgotar rate limit do GHL.

    Se a nova coleta trouxer muito menos contatos que o cache anterior
    (ex: rate limit), mantem o cache antigo pra nao regredir os numeros.
    r   rd   r   )r  contatos_analisadosr   g?r  r$   r%   ultima_tentativa_tsultima_tentativa_size)r	  rk  
_ATRIB_TTLr   r  rj  r>  r2   r   r'   r   r(   r)   )r1   r  forcer'   r[  r  	resultadoolds           r,   coletar_atribuicao_cachedrt  V  s    ))+C\&)s\$5G/G:.UF###%H(+3OH#Hh7I'*8}I#$ 

6
"C
sww,a03x=33FF3x=[^K^%-\\(,,%?%I%IS\%I%]!"'*8}#$ T
$LLr+   c           
        | j                  di       }|j                  di       }|j                  di       }dd} |||j                  dd      |j                  dd            } |||j                  dd      |j                  dd            }||z   }	|d   |d   z   }
|d   |d   z   }||t        |	d      ||
|
rt        |	|
z  d      nd	|rt        |	|z  d      nd	d
dS )z
    Retorna:
      {
        'meta':   {'gasto': X, 'compras': N, 'cpa_real': Y, 'cpl_real': Z, 'leads': N},
        'google': {...},
        'total':  {'gasto': X, 'compras': N, 'cpa_real': Y}
      }
    r   r   r   r  r  c                n    t        | d      |||rt        | |z  d      nd|rt        | |z  d      dS ddS )Nr   r=  gastor  r  cpa_realcpl_real)rY  )rx  r  r  s      r,   _blocoz!calcular_cpa_real.<locals>._bloco  sM    5!_5<egoq1#38eemQ/
 	

 ?B
 	
r+   r   r   r=  rw  )r   r   r   )rx  rR  r  intr  r|  r   r8   )r2   rY  )
atribuicao
gasto_metagasto_googler   r   r   r{  
bloco_metabloco_googlegasto_totalcompras_totalleads_totals               r,   calcular_cpa_realr  v  s    {B/I==$D]]8R(F
 
DHHWa$8$((9a:PQJ,

7A(>

9VW@XYL|+Ky)L,CCMW%W(==K ;* $ANkM91=TW?JkK7;PS

 
r+   )r   r{   )r   r8   )rL   r   rM   r8   r   None)rT   z
str | Noner   r{   )r[   r{   r\   r{   r   r{   )rL   r   r   zIterable[dict])rd   r{   r   datetime | None)r   r   )r   r   r   r8   )r   r   r   r8   )r   zlist[dict] | Noner   r{   )(   g?)
r1   r8   r  r{   r  r|  r  rR  r   r   )r>   r{   r   r  )r  r   r[  r8   r   r8   )F)r1   r8   r  r{   rq  boolr   r8   )r}  r8   r~  rR  r  rR  r   r8   )>__doc__
__future__r   rI   osrR   r	  urllib.parserX   r   r   r   pathlibr   typingr   r  flaskr	   r
   r   r   environr2   TRACKING_DIRmkdirri   r|   rQ   r:   r   r   r   r   __name__bpr-   r7   r?   rO   rV   r^   routerk   r   r   r   r   r   r   r   r   r   r   r  r  rj  rk  __annotations__rp  rt  r  r*   r+   r,   <module>r     sA  
 #  	 	   2 2    7 7
 JJNN5   4$  /^+ 11 ::>>./B 
 ,-	     z8$DI& +w': (:* +v/& 0&R +	{+ ,"*--`2>$T 	::: : 	:
 :zCN ##.d .
   
	@+++ + 
	+r+   