feat: link enviado como mensagem direta + email extraction + contact metadata auto-persist
- GeneratePixTool: envia payment_link como mensagem outgoing direta (bypassa hallucination de [Link do Pix] placeholder pela LLM) - GeneratePixTool: extrai email das mensagens recentes via regex e persiste em contact.email - GenerateReservationLinkTool: mesmo padrao de envio direto do link - Captain::Reservation: after_create_commit callback atualiza ultima_suite/permanencia/reserva_em/total_reservas em contact.custom_attributes (aparece no painel lateral)
This commit is contained in:
parent
7c9411a0b0
commit
f8d64b6992
@ -88,6 +88,7 @@ class Captain::Reservation < ApplicationRecord
|
|||||||
|
|
||||||
before_validation :set_captain_unit_id, on: :create
|
before_validation :set_captain_unit_id, on: :create
|
||||||
after_commit :sync_conversation_marker_snapshot
|
after_commit :sync_conversation_marker_snapshot
|
||||||
|
after_create_commit :update_contact_reservation_metadata
|
||||||
|
|
||||||
def ui_status
|
def ui_status
|
||||||
Captain::Reservations::MarkerBuilder.ui_status(status)
|
Captain::Reservations::MarkerBuilder.ui_status(status)
|
||||||
@ -125,4 +126,23 @@ class Captain::Reservation < ApplicationRecord
|
|||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error("[Captain::Reservation] failed to sync conversation marker: #{e.class} - #{e.message}")
|
Rails.logger.error("[Captain::Reservation] failed to sync conversation marker: #{e.class} - #{e.message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Atualiza campos visiveis no painel lateral do Chatwoot (custom_attributes)
|
||||||
|
# pra que a recepcionista veja num relance:
|
||||||
|
# ultima_suite, ultima_permanencia, ultima_reserva_em, total_reservas
|
||||||
|
def update_contact_reservation_metadata # rubocop:disable Metrics/AbcSize
|
||||||
|
return if contact.blank?
|
||||||
|
|
||||||
|
meta = metadata.to_h
|
||||||
|
current = contact.custom_attributes.to_h
|
||||||
|
|
||||||
|
current['ultima_suite'] = meta['category'].presence || suite_identifier.to_s.split('·').first.to_s.strip
|
||||||
|
current['ultima_permanencia'] = meta['stay_type'].presence || suite_identifier.to_s.split('·').last.to_s.strip
|
||||||
|
current['ultima_reserva_em'] = created_at.iso8601
|
||||||
|
current['total_reservas'] = (current['total_reservas'].to_i + 1)
|
||||||
|
|
||||||
|
contact.update_columns(custom_attributes: current) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.warn("[Captain::Reservation] failed to update contact metadata: #{e.class} - #{e.message}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -2,6 +2,7 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
|||||||
CPF_WITH_LABEL_REGEX = /cpf[^\d]*(\d{3}\.?\d{3}\.?\d{3}-?\d{2}|\d{11})/i
|
CPF_WITH_LABEL_REGEX = /cpf[^\d]*(\d{3}\.?\d{3}\.?\d{3}-?\d{2}|\d{11})/i
|
||||||
CPF_FALLBACK_REGEX = /\b\d{11}\b/
|
CPF_FALLBACK_REGEX = /\b\d{11}\b/
|
||||||
NAME_WITH_LABEL_REGEX = /nome\s*[:\-]\s*([^\n\r,;]+)/i
|
NAME_WITH_LABEL_REGEX = /nome\s*[:\-]\s*([^\n\r,;]+)/i
|
||||||
|
EMAIL_REGEX = /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/
|
||||||
SUITE_REGEX = /su[ií]te\s+([^\n\r,.!?]+)/i
|
SUITE_REGEX = /su[ií]te\s+([^\n\r,.!?]+)/i
|
||||||
DDMMYYYY_REGEX = %r{\b(\d{1,2}/\d{1,2}/\d{2,4})\b}
|
DDMMYYYY_REGEX = %r{\b(\d{1,2}/\d{1,2}/\d{2,4})\b}
|
||||||
CURRENCY_REGEX = /r\$\s*([\d.,]+)/i
|
CURRENCY_REGEX = /r\$\s*([\d.,]+)/i
|
||||||
@ -116,6 +117,8 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
|||||||
|
|
||||||
updates[:name] = extracted[:name] if valid_contact_name?(contact.name).blank? && extracted[:name].present?
|
updates[:name] = extracted[:name] if valid_contact_name?(contact.name).blank? && extracted[:name].present?
|
||||||
|
|
||||||
|
updates[:email] = extracted[:email] if contact.email.blank? && extracted[:email].present?
|
||||||
|
|
||||||
return if updates.blank?
|
return if updates.blank?
|
||||||
|
|
||||||
contact.update!(updates)
|
contact.update!(updates)
|
||||||
@ -132,6 +135,7 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
|||||||
|
|
||||||
cpf = nil
|
cpf = nil
|
||||||
name = nil
|
name = nil
|
||||||
|
email = nil
|
||||||
|
|
||||||
recent_messages.each do |message|
|
recent_messages.each do |message|
|
||||||
content = normalize_text(message.content)
|
content = normalize_text(message.content)
|
||||||
@ -139,15 +143,23 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
|||||||
|
|
||||||
cpf ||= extract_cpf_from_text(content)
|
cpf ||= extract_cpf_from_text(content)
|
||||||
name ||= extract_name_from_text(content)
|
name ||= extract_name_from_text(content)
|
||||||
break if cpf.present? && name.present?
|
email ||= extract_email_from_text(content)
|
||||||
|
break if cpf.present? && name.present? && email.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
cpf: cpf,
|
cpf: cpf,
|
||||||
name: name
|
name: name,
|
||||||
|
email: email
|
||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def extract_email_from_text(text)
|
||||||
|
text = normalize_text(text)
|
||||||
|
candidate = text[EMAIL_REGEX]
|
||||||
|
candidate.to_s.strip.downcase.presence
|
||||||
|
end
|
||||||
|
|
||||||
def extract_cpf_from_text(text)
|
def extract_cpf_from_text(text)
|
||||||
text = normalize_text(text)
|
text = normalize_text(text)
|
||||||
candidate = text[CPF_WITH_LABEL_REGEX, 1]
|
candidate = text[CPF_WITH_LABEL_REGEX, 1]
|
||||||
@ -317,6 +329,18 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
|||||||
normalize_payload({ formatted_message: msg, success: true })
|
normalize_payload({ formatted_message: msg, success: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dispatch_direct_link_message(link, label)
|
||||||
|
return if @conversation.blank? || link.to_s.strip.empty?
|
||||||
|
|
||||||
|
content = "#{label}\n#{link}".strip
|
||||||
|
Messages::MessageBuilder.new(@assistant, @conversation, {
|
||||||
|
content: content,
|
||||||
|
message_type: 'outgoing'
|
||||||
|
}).perform
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.warn("[GeneratePixTool] failed to dispatch direct link: #{e.class} - #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
def current_pix_charge_for(reservation)
|
def current_pix_charge_for(reservation)
|
||||||
return nil unless reservation
|
return nil unless reservation
|
||||||
|
|
||||||
@ -333,7 +357,13 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
|||||||
Rails.logger.info "[GeneratePixTool] Reserva #{reservation.id} → pending_payment"
|
Rails.logger.info "[GeneratePixTool] Reserva #{reservation.id} → pending_payment"
|
||||||
|
|
||||||
final_prefix = prefix || 'Cobrança Pix gerada com sucesso.'
|
final_prefix = prefix || 'Cobrança Pix gerada com sucesso.'
|
||||||
build_pix_response(charge, reservation, amount: charge_amount, prefix: final_prefix)
|
response = build_pix_response(charge, reservation, amount: charge_amount, prefix: final_prefix)
|
||||||
|
|
||||||
|
# Envia o link como mensagem direta pro cliente. Isso garante que o URL chegue
|
||||||
|
# no WhatsApp mesmo que a LLM parafraseie com placeholder tipo "[Link do Pix]".
|
||||||
|
dispatch_direct_link_message(response[:payment_link], 'Link do Pix:')
|
||||||
|
|
||||||
|
response
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
safe_error_message = normalize_text(e.message)
|
safe_error_message = normalize_text(e.message)
|
||||||
Rails.logger.error("[GeneratePixTool] Falha ao gerar Pix: #{e.class} - #{safe_error_message}")
|
Rails.logger.error("[GeneratePixTool] Falha ao gerar Pix: #{e.class} - #{safe_error_message}")
|
||||||
|
|||||||
@ -81,6 +81,10 @@ class Captain::Tools::GenerateReservationLinkTool < Captain::Tools::BaseTool
|
|||||||
query = build_query(enriched_params)
|
query = build_query(enriched_params)
|
||||||
url = "#{base}/?#{query}"
|
url = "#{base}/?#{query}"
|
||||||
|
|
||||||
|
# Envia o link como mensagem direta pra garantir que chegue no WhatsApp
|
||||||
|
# mesmo que a LLM parafraseie com placeholder [Link da reserva] etc.
|
||||||
|
dispatch_direct_link_message(conversation, url)
|
||||||
|
|
||||||
{
|
{
|
||||||
formatted_message: url,
|
formatted_message: url,
|
||||||
raw_payload: url,
|
raw_payload: url,
|
||||||
@ -117,6 +121,18 @@ class Captain::Tools::GenerateReservationLinkTool < Captain::Tools::BaseTool
|
|||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dispatch_direct_link_message(conversation, url)
|
||||||
|
return if conversation.blank? || url.to_s.strip.empty?
|
||||||
|
|
||||||
|
content = "Link da sua reserva (tudo ja preenchido):\n#{url}"
|
||||||
|
Messages::MessageBuilder.new(@assistant, conversation, {
|
||||||
|
content: content,
|
||||||
|
message_type: 'outgoing'
|
||||||
|
}).perform
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.warn("[GenerateReservationLinkTool] failed to dispatch link: #{e.class} - #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
def build_query(actual_params)
|
def build_query(actual_params)
|
||||||
mapping = {
|
mapping = {
|
||||||
marca: actual_params[:marca],
|
marca: actual_params[:marca],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user