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
|
||||
after_commit :sync_conversation_marker_snapshot
|
||||
after_create_commit :update_contact_reservation_metadata
|
||||
|
||||
def ui_status
|
||||
Captain::Reservations::MarkerBuilder.ui_status(status)
|
||||
@ -125,4 +126,23 @@ class Captain::Reservation < ApplicationRecord
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[Captain::Reservation] failed to sync conversation marker: #{e.class} - #{e.message}")
|
||||
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
|
||||
|
||||
@ -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_FALLBACK_REGEX = /\b\d{11}\b/
|
||||
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
|
||||
DDMMYYYY_REGEX = %r{\b(\d{1,2}/\d{1,2}/\d{2,4})\b}
|
||||
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[:email] = extracted[:email] if contact.email.blank? && extracted[:email].present?
|
||||
|
||||
return if updates.blank?
|
||||
|
||||
contact.update!(updates)
|
||||
@ -132,6 +135,7 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
||||
|
||||
cpf = nil
|
||||
name = nil
|
||||
email = nil
|
||||
|
||||
recent_messages.each do |message|
|
||||
content = normalize_text(message.content)
|
||||
@ -139,15 +143,23 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
||||
|
||||
cpf ||= extract_cpf_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
|
||||
|
||||
{
|
||||
cpf: cpf,
|
||||
name: name
|
||||
name: name,
|
||||
email: email
|
||||
}.compact
|
||||
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)
|
||||
text = normalize_text(text)
|
||||
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 })
|
||||
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)
|
||||
return nil unless reservation
|
||||
|
||||
@ -333,7 +357,13 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
|
||||
Rails.logger.info "[GeneratePixTool] Reserva #{reservation.id} → pending_payment"
|
||||
|
||||
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
|
||||
safe_error_message = normalize_text(e.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)
|
||||
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,
|
||||
raw_payload: url,
|
||||
@ -117,6 +121,18 @@ class Captain::Tools::GenerateReservationLinkTool < Captain::Tools::BaseTool
|
||||
}.compact
|
||||
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)
|
||||
mapping = {
|
||||
marca: actual_params[:marca],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user