
    -zjq>                       d Z ddlmZ ddlZddlmZ ddlZ ej                  d      ZdZ	dZ
ddZ	 d	 	 	 	 	 	 	 	 	 dd	Z	 d	 	 	 	 	 	 	 	 	 dd
Z	 	 d	 	 	 	 	 	 	 	 	 ddZ	 d	 	 	 	 	 	 	 	 	 ddZ	 	 d	 	 	 	 	 	 	 	 	 	 	 d dZd!dZ	 d	 	 	 	 	 	 	 	 	 d"dZ	 	 	 	 	 	 	 	 	 	 d#dZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 d$dZ	 	 	 	 	 	 	 	 	 	 d%dZd&dZ	 d	 	 	 	 	 	 	 	 	 	 	 	 	 d'dZ	 	 	 	 	 	 	 	 d(dZy))u  
GoHighLevel (Linkia) API client — Climb / TORQ.

Wrappers:
  - send_whatsapp_message: envia msg WhatsApp de volta ao lead
  - get_conversation_messages: busca historico da conversa
  - get_contact: recupera contato + custom fields
  - update_contact_custom_fields: atualiza campos custom sdr__
  - move_opportunity_stage: move opportunity entre stages do pipeline
  - list_custom_fields: lista custom fields da location
  - create_custom_field: cria custom field (usado no setup)
    )annotationsN)Any
ghl_clientz$https://services.leadconnectorhq.comz
2021-07-28c                    d|  ddt         dS )NzBearer application/json)AuthorizationContent-TypeAcceptVersion)GHL_API_VERSION)tokens    G/opt/mia/workspace/clientes/torq-brasil/sdr_bruno_webhook/ghl_client.py_headersr      s    "5'**$"	     c                   ddl }|j                  dd| xs d      }|sy|j                  d       d}|dd	}||d
d}	 t        j                  |||d      }	|	j
                  dv }
|
rr	 |	j                         }t        |t              rP|j                  d      du r=t        j                  d||	j                  dd        d|	j
                  |	j                  fS n/t        j                  d||	j
                  |	j                  dd        |
|	j
                  |	j                  fS # t        $ r Y $w xY w# t        j                  $ r.}t        j                  d|       ddt!        |      fcY d}~S d}~ww xY w)u   Envia msg direto na Dinastia API — chega no WhatsApp do lead em tempo real.

    Complementa o send_whatsapp_message (que registra no CRM Linkia).
    Usar os dois em paralelo: Linkia pra histórico, Dinastia pra entrega.
    r   Nz\D )Fr   zsem telefone/z/chat/send/textr   )r   r	   T)PhoneBodyNumberCheck   )headersjsontimeout)      successFz,Dinastia send success=false phone=%s body=%sr   z/Dinastia send failed phone=%s status=%s body=%szDinastia send exception: %s)resubrstriprequestspoststatus_coder   
isinstancedictgetlogwarningtext	ExceptionRequestException	exceptionstr)phonemessagedinastia_tokendinastia_base_refoneurlr   bodyrokdataes                r   send_via_dinastiar:   (   s`    775"ekr*D'!!#&'
7C&8JKG74@D MM#wT2F]]j(vvxdD)dhhy.AU.JKK N $affTcl4 !--77 KKIammQVVDS\;1==!&&((   $$  3Q7aQ sC   )D1 )A/D" AD1 "	D.+D1 -D..D1 1E2#E-'E2-E2c                   t          d}d| |d}|r||d<   	 t        j                  ||t        |      d      }|j                  }|s/t
        j                  d| |j                  |j                  dd	        ||j                  |j                  fS # t        j                  $ r.}t
        j                  d
|       ddt        |      fcY d}~S d}~ww xY w)z2Envia mensagem WhatsApp via GHL Conversations API.z/conversations/messagesWhatsApp)type	contactIdr/   conversationId   r   r   r   z,GHL send failed contact=%s status=%s body=%sN  zGHL send exception: %sFr   GHL_API_BASEr!   r"   r   r7   r'   r(   r#   r)   r+   r,   r-   )	
contact_idr/   r   conversation_idr4   payloadr6   r7   r9   s	            r   send_whatsapp_messagerH   O   s     N1
2CG
 $3 !	 MM#GXe_bQTTKKF"AMM166$3<A1==!&&(($$  .2aQ s   A7B C##CCCc                    | sg S t          d|  d}d|i}|r||d<   	 t        j                  |t        |      |d      }|j                  s0t
        j                  d|j                  |j                  dd	        g S |j                         xs i }|j                  d
      }t        |t              r|j                  d
g       }|xs g S # t        j                  $ r"}	t
        j                  d|	       g cY d}	~	S d}	~	ww xY w)zBusca historico de mensagens de uma conversa (mais recentes primeiro).

    `types` opcional: filtro tipo GHL (ex: 'TYPE_SMS,TYPE_WHATSAPP').
    z/conversations/z	/messageslimitr=   r   r   paramsr   z6GHL get_conversation_messages failed status=%s body=%sNrB   messagesz+GHL get_conversation_messages exception: %s)rD   r!   r&   r   r7   r'   r(   r#   r)   r   r$   r%   r+   r,   )
rF   r   rJ   typesr4   rL   r6   r8   msgsr9   s
             r   get_conversation_messagesrP   j   s     	N//):)
DC%u-FvLLhuo &4ttKKPqvvds|5Ivvx~2 xx
#dD!88J+Dzr$$ CQG	s%   AC ;AC C=C82C=8C=c                   | r|sg S t          d}|| |d}	 t        j                  |t        |      |d      }|j                  s0t
        j                  d|j                  |j                  dd        g S |j                         xs i }|j                  d      xs |j                  d	      xs g }|xs g S # t        j                  $ r"}	t
        j                  d
|	       g cY d}	~	S d}	~	ww xY w)zProcura conversas de um contato (mais recente primeiro).

    Endpoint: GET /conversations/search?locationId=X&contactId=Y&limit=N
    Retorna lista de {id, contactId, lastMessageBody, ...}.
    z/conversations/search)
locationIdr>   rJ      rK   z1GHL search_conversations failed status=%s body=%sNrB   conversationsr8   z&GHL search_conversations exception: %srD   r!   r&   r   r7   r'   r(   r#   r)   r   r+   r,   )
rE   location_idr   rJ   r4   rL   r6   r8   convsr9   s
             r   search_conversationsrX      s     [	N/
0C!F
LLhuo &4ttKKKqvvds|5Ivvx~2)CTXXf-=C{$$ >B	s%   AB7 5AB7 7C,
C'!C,'C,c           	        ddl }d}t        d|dz         D ]T  }t        | ||d      }|sI||k  r+t        j	                  d| |||       |j                  |       Et        j	                  d| |        y|d   }	|	j                  d      xs |	j                  d	      }|st        j	                  d
|         yt        ||dd      }
|
D ]V  }|j                  d      xs dj                         dk7  r*|j                  d      xs dj                         }|sP||fc c S  ||k  r-t        j	                  d| ||||       |j                  |       =t        j	                  d| ||       W d|fS )u  Busca a ultima mensagem INBOUND do contato via GHL, com retry.

    Retorna (body, conversation_id) — ambos podem ser None se nao houver.
    Fluxo: search_conversations -> pega conv mais recente -> lista mensagens
    filtrando WhatsApp/SMS -> pega a primeira inbound (mais recente).

    Race condition tratada: o GHL leva 3-15s pra indexar conversa recém-criada
    pelo endpoint /conversations/search. Se nao achar, faz retry com backoff.
    r   N   )rJ   zR[fetch_last_inbound] contact=%s tentativa %d/%d sem conversa GHL, aguardando %.1fszC[fetch_last_inbound] contact=%s sem conversa GHL apos %d tentativas)NNid_idz+[fetch_last_inbound] contact=%s conv sem id
   zTYPE_SMS,TYPE_WHATSAPP)rJ   rN   	directionr   inboundr5   z`[fetch_last_inbound] contact=%s conv=%s sem msg inbound util, aguardando %.1fs (tentativa %d/%d)zO[fetch_last_inbound] contact=%s conv=%s sem msg inbound util apos %d tentativas)
timerangerX   r'   infosleepr&   rP   lowerstrip)rE   rV   r   max_attempts	backoff_sr`   conv_idattemptrW   convrO   mr5   s                r   fetch_last_inbound_messagerl      s     GL1,- !4$Ze1M%m#WlIG

9%HHZ/Qx((4.3DHHUOHHBJO(U",D
  	%Ak"(b//1Y>EE&M'R..0DW}$	% \!HHw)WlLJJy!bWl	4A!4D =r   c                   t          d|  }	 t        j                  |t        |      d      }|j                  s/t
        j                  d|j                  |j                  d d        y |j                         xs i }|j                  d      xs |S # t        j                  $ r }t
        j                  d|       Y d }~y d }~ww xY w)N
/contacts/rS   r   r   z(GHL get_contact failed status=%s body=%srB   contactzGHL get_contact exception: %srU   )rE   r   r4   r6   r8   r9   s         r   get_contactrq      s    N*ZL
1C
LLhuorBttKKBqvvds|5vvx~2xx	"*d*$$ 5q9s   AB )(B C%C  Cc                J   |sy|st         j                  d       yg }|j                         D ]C  \  }}|j                  |      }|st         j                  d|       0|j	                  ||d       E |syt
         d|  }d|i}		 t        j                  ||	t        |      d	      }
|
j                  s.t         j                  d
|
j                  |
j                  dd        |
j                  S # t        j                  $ r }t         j                  d|       Y d}~yd}~ww xY w)z
    Atualiza custom fields de um contato.
    `fields` = {"sdr__nome_lead": "Marcos", "sdr__cidade": "BH", ...}
    `field_id_map` = {"sdr__nome_lead": "aBc123XyZ", ...} (obrigatorio na v2 da API)
    Tu<   update_contact_custom_fields sem field_id_map — nada feitoFu%   campo %s sem ID mapeado — ignorando)r[   field_valuern   customFieldsr   rA   z+GHL update_contact failed status=%s body=%sNrB   z GHL update_contact exception: %s)r'   r(   itemsr&   appendrD   r!   putr   r7   r#   r)   r+   r,   )rE   fieldsr   field_id_mappayload_fieldskeyvalfidr4   rG   r6   r9   s               r   update_contact_custom_fieldsr~      s    RSNLLN ?Ss#KK?ES=>? N*ZL
1C~.GLL7HUORPttKKEqvvds|5tt$$ 8!<s   A'C/ /D"DD"c                   t          d}|| |dd}	 t        j                  |t        |      |d      }|j                  s/t
        j                  d|j                  |j                  dd        y|j                         xs i }|j                  d	g       }|r|d
   S dS # t        j                  $ r }	t
        j                  d|	       Y d}	~	yd}	~	ww xY w)z1Procura opportunity ativa do contato no pipeline.z/opportunities/searchr   )rV   rE   pipeline_idrJ   rS   rK   z/GHL search opportunity failed status=%s body=%sNrB   opportunitiesr   z$GHL search opportunity exception: %srU   )
rE   r   r   rV   r4   rL   r6   r8   oppsr9   s
             r   find_opportunity_for_contactr   &  s     N/
0C" "	FLLhuofbQttKKIqvvds|5vvx~2xx,tAw(D($$ <a@s$   AB /,B B C1CCc                   t          d}||||d| d}	 t        j                  ||t        |      d      }|j                  s/t
        j                  d|j                  |j                  d d        y |j                         xs i j                  d      S # t        j                  $ r }	t
        j                  d	|	       Y d }	~	y d }	~	ww xY w)
N/opportunities/open)
pipelineIdrR   namepipelineStageIdstatusr>   r   rA   z/GHL create opportunity failed status=%s body=%srB   opportunityz$GHL create opportunity exception: %s)rD   r!   r"   r   r7   r'   r(   r#   r)   r   r&   r+   r,   )
rE   r   stage_idr   r   rV   r4   rG   r6   r9   s
             r   create_opportunityr   B  s     N/
*C!!#G	MM#GXe_bQttKKIqvvds|5B##M22$$ <a@s   AB 1"B C'CCc                `   t          d|  }||d}	 t        j                  ||t        |      d      }|j                  s.t
        j                  d|j                  |j                  d d        |j                  S # t        j                  $ r }t
        j                  d|       Y d }~yd }~ww xY w)	Nr   )r   r   rS   rA   z-GHL move opportunity failed status=%s body=%srB   z"GHL move opportunity exception: %sF)rD   r!   rw   r   r7   r'   r(   r#   r)   r+   r,   )opportunity_idr   r   r   r4   rG   r6   r9   s           r   move_opportunity_stager   _  s     N/.)9
:C!#GLL7HUORPttKKGqvvds|5tt$$ :A>s   A'A: :B-B((B-c                   t          d|  d}	 t        j                  |t        |      d      }|j                  s0t
        j                  d|j                  |j                  dd        g S |j                         xs i j                  dg       S # t        j                  $ r"}t
        j                  d	|       g cY d}~S d}~ww xY w)
z)Lista todos os custom fields da location./locations//customFieldsrS   ro   z/GHL list_custom_fields failed status=%s body=%sNrB   rt   z$GHL list_custom_fields exception: %srU   )rV   r   r4   r6   r9   s        r   list_custom_fieldsr   y  s    N+k]-
@C	LLhuorBttKKIqvvds|5IB##NB77$$ <a@	s$   AB +#B C"B?9C?Cc                8   t          d|  d}|||d}|rt        |      |d<   	 t        j                  ||t	        |      d      }|j
                  dk(  r3d|j                  j                         v rt        j                  d	|       y
|j                  s/t        j                  d|j
                  |j                  d
d        y
|j                         xs i j                  d      S # t        j                  $ r }	t        j                  d|	       Y d
}	~	y
d
}	~	ww xY w)z
    Cria custom field na location.
    data_type: TEXT, LARGE_TEXT, NUMERICAL, PHONE, MONETORY, CHECKBOX, DATE,
               SINGLE_OPTIONS (dropdown), MULTIPLE_OPTIONS, RADIO, FILE_UPLOAD
    r   r   )r   dataTypefieldKeyoptionsr   rA   rB   alreadyzcampo %s ja existeNz0GHL create_custom_field failed status=%s body=%scustomFieldz%GHL create_custom_field exception: %s)rD   listr!   r"   r   r#   r)   rd   r'   rb   r7   r(   r   r&   r+   r,   )
rV   r   	field_key	data_typer   picklist_optionsr4   rG   r6   r9   s
             r   create_custom_fieldr     s     N+k]-
@CG
 !"23	MM#GXe_bQ==CI$?HH)95ttKKJqvvds|5B##M22$$ =qAs$   A#C& :C& "C& &D9DDc                   t          d|  d}d|i}	 t        j                  ||t        |      d      }|j                  }|s/t
        j                  d| |j                  |j                  dd        ||j                  |j                  fS # t        j                  $ r.}t
        j                  d	|       d
dt        |      fcY d}~S d}~ww xY w)zCria uma NOTA interna no contato (nao dispara canal WhatsApp/SMS).

    Usado pra registrar as respostas do Bruno no CRM sem acionar API oficial.
    Endpoint: POST /contacts/{contactId}/notes
    rn   z/notesr5   rS   rA   z0GHL add_note failed contact=%s status=%s body=%sNi,  zGHL add_note exception: %sFr   rC   )rE   r5   r   r4   rG   r6   r7   r9   s           r   add_contact_noter     s     N*ZL
7CtnG	 MM#GXe_bQTTKKJ"AMM166$3<A1==!&&(($$  2A6aQ s   A7B
 
C#C CC)r   r-   returnzdict[str, str])z"https://dinastiapi.agentesclimb.us)
r.   r-   r/   r-   r0   r-   r1   r-   r   tuple[bool, int, str])N)
rE   r-   r/   r-   r   r-   rF   
str | Noner   r   )r@   N)
rF   r-   r   r-   rJ   intrN   r   r   list[dict[str, Any]])rZ   )
rE   r-   rV   r-   r   r-   rJ   r   r   r   )   g      @)rE   r-   rV   r-   r   r-   rf   r   rg   floatr   ztuple[str | None, str | None])rE   r-   r   r-   r   dict[str, Any] | None)
rE   r-   rx   zdict[str, Any]r   r-   ry   zdict[str, str] | Noner   bool)
rE   r-   r   r-   r   r-   rV   r-   r   r   )rE   r-   r   r-   r   r-   r   r-   r   r-   rV   r-   r   r   )
r   r-   r   r-   r   r-   r   r-   r   r   )rV   r-   r   r-   r   r   )rV   r-   r   r-   r   r-   r   r-   r   r-   r   zlist[str] | Noner   r   )rE   r-   r5   r-   r   r-   r   r   )__doc__
__future__r   loggingtypingr   r!   	getLoggerr'   rD   r   r   r:   rH   rP   rX   rl   rq   r~   r   r   r   r   r   r    r   r   <module>r      s   #   g%5" >	$ $ $  $  	$ 
 $ V #'	       	 
  < 	      	 
  N 	  	
 L 444 4 	4
 4 #4v& +/	&&& & (	&
 
&Z  	
 8  	
   :  	
 
4* *.##
# # 	#
 # '# #L  
    	 r   