From 7c9411a0b0e61131103431dcbdc501e2a03a6948 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Tue, 14 Apr 2026 13:26:02 -0300 Subject: [PATCH] feat: persiste metadados do cliente em custom_attributes + tool blindada - Controller grava cpf/ultima_suite/ultima_permanencia/ultima_reserva_em/total_reservas em contact.custom_attributes (aparece no painel lateral do Chatwoot) - GenerateReservationLinkTool exige marca/unidade/categoria/permanencia/checkin_at; retorna erro se Jasmine chamar sem esses dados --- .../captain/public_reservations_controller.rb | 34 ++++++++++- .../tools/generate_reservation_link_tool.rb | 61 ++++++++++--------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/app/controllers/public/api/v1/captain/public_reservations_controller.rb b/app/controllers/public/api/v1/captain/public_reservations_controller.rb index 196b59f01..017ce5fb1 100644 --- a/app/controllers/public/api/v1/captain/public_reservations_controller.rb +++ b/app/controllers/public/api/v1/captain/public_reservations_controller.rb @@ -31,13 +31,19 @@ class Public::Api::V1::Captain::PublicReservationsController < ActionController: name: customer[:name], phone_number: normalized_phone, email: customer[:email].presence, + custom_attributes: { + cpf: customer[:cpf].presence + }.compact, additional_attributes: { - cpf: customer[:cpf].presence, origem: 'reserva-1001' - }.compact + } } ).perform + # Atualiza campos visiveis do contato (alem do ContactInboxBuilder, que so + # preenche no create e nao mexe em contato ja existente). + persist_customer_metadata!(contact_inbox.contact, customer, params) + conversation = ConversationBuilder.new( params: ActionController::Parameters.new( additional_attributes: { @@ -130,6 +136,30 @@ class Public::Api::V1::Captain::PublicReservationsController < ActionController: render json: { error: 'unauthorized' }, status: :unauthorized end + # Persiste CPF, email, ultima suite/permanencia, data e total de reservas + # no contact.custom_attributes para aparecer no painel lateral do Chatwoot + # e pra facilitar reservas futuras (cliente recorrente). + def persist_customer_metadata!(contact, customer, payload) # rubocop:disable Metrics/AbcSize + return if contact.blank? + + current_custom = contact.custom_attributes || {} + current_custom = current_custom.dup + + current_custom['cpf'] = customer[:cpf] if customer[:cpf].present? + current_custom['ultima_suite'] = payload[:category] if payload[:category].present? + current_custom['ultima_permanencia'] = payload[:stay_type] if payload[:stay_type].present? + current_custom['ultima_reserva_em'] = Time.current.iso8601 + current_custom['total_reservas'] = (current_custom['total_reservas'].to_i + 1) + + updates = { custom_attributes: current_custom } + updates[:email] = customer[:email] if customer[:email].present? && contact.email.blank? + updates[:name] = customer[:name] if customer[:name].present? && contact.name.blank? + + contact.update!(updates) + rescue StandardError => e + Rails.logger.error("[PublicReservations] persist_customer_metadata failed: #{e.message}") + end + # Espelha Captain::Tools::GeneratePixTool#mark_conversation_as_awaiting_payment # (enterprise/app/services/captain/tools/generate_pix_tool.rb:713-721) def mark_conversation_as_awaiting_payment(conversation) diff --git a/enterprise/app/services/captain/tools/generate_reservation_link_tool.rb b/enterprise/app/services/captain/tools/generate_reservation_link_tool.rb index 9e7d57fe4..c1a8c0dfb 100644 --- a/enterprise/app/services/captain/tools/generate_reservation_link_tool.rb +++ b/enterprise/app/services/captain/tools/generate_reservation_link_tool.rb @@ -14,74 +14,75 @@ class Captain::Tools::GenerateReservationLinkTool < Captain::Tools::BaseTool def description <<~DESC.strip FALLBACK para gerar reserva via pagina publica (Reserva Rede 1001). - Use SOMENTE quando `generate_pix` falhar ou retornar success=false. - Gera um link com os dados ja preenchidos (categoria, permanencia, - data). Nome, telefone e CPF do cliente sao lidos automaticamente do - contato da conversa - voce nao precisa passa-los. Envie o link - retornado como alternativa, explicando que o cliente pode revisar - e pagar pela pagina. + Use SOMENTE quando `generate_pix` retornar success=false na mesma conversa. + OBRIGATORIO passar: marca, unidade, categoria, permanencia, checkin_at. + Nome/telefone/CPF/email vem do contato automaticamente. + Retorna uma URL que DEVE ser enviada ao cliente como texto puro (sem markdown). DESC end def tool_parameters_schema { type: 'object', + required: %w[marca unidade categoria permanencia checkin_at], properties: { marca: { type: 'string', - description: 'Nome da marca. Ex: "Hotel 1001 Noites".' + description: 'OBRIGATORIO. Nome da marca. Ex: "Hotel 1001 Noites Prime".' }, unidade: { type: 'string', - description: 'Nome da unidade do hotel. Ex: "Hotel 1001 Aguas Lindas".' - }, - permanencia: { - type: 'string', - description: 'Permanencia escolhida. Ex: "3hrs", "4hrs", "Pernoite".' + description: 'OBRIGATORIO. Nome da unidade. Ex: "Prime Aguas Lindas".' }, categoria: { type: 'string', - description: 'Categoria da suite. Ex: "Standard", "Hidromassagem".' + description: 'OBRIGATORIO. Categoria da suite. Ex: "Alexa", "Stilo", "Hidromassagem".' + }, + permanencia: { + type: 'string', + description: 'OBRIGATORIO. Permanencia escolhida. Ex: "2hrs", "3hrs", "4hrs", "Pernoite", "Diaria".' }, checkin_at: { type: 'string', - description: 'Data e horario de check-in em ISO 8601. Ex: "2026-04-14T22:00:00".' - }, - nome: { - type: 'string', - description: 'Nome completo do cliente.' - }, - telefone: { - type: 'string', - description: 'Telefone do cliente (com ou sem DDI).' - }, - cpf: { - type: 'string', - description: 'CPF do cliente (apenas numeros ou com pontuacao).' + description: 'OBRIGATORIO. Check-in em ISO 8601. Ex: "2026-04-28T22:00:00". Default: ' \ + '22:00 para Pernoite/Diaria, +1h do horario atual para 2hrs/3hrs/4hrs.' }, email: { type: 'string', - description: 'Email do cliente (opcional).' + description: 'OPCIONAL. Email do cliente (se tiver).' }, observacao: { type: 'string', - description: 'Observacao ou preferencia especial do cliente (opcional).' + description: 'OPCIONAL. Preferencia especial do cliente.' } } } end + REQUIRED_STAY_FIELDS = %i[marca unidade categoria permanencia checkin_at].freeze + def execute(*args, **params) actual_params = resolve_params(args, params) + missing = REQUIRED_STAY_FIELDS.reject { |f| actual_params[f].to_s.strip.present? } + + if missing.any? + return { + formatted_message: "Nao posso gerar o link sem: #{missing.join(', ')}. " \ + 'Colete esses dados na conversa e chame a ferramenta de novo.', + success: false, + missing_fields: missing + } + end + conversation = resolve_conversation(args, params) enriched_params = enrich_with_contact(actual_params, conversation) base = ENV.fetch('RESERVA_1001_BASE_URL', DEFAULT_BASE_URL) query = build_query(enriched_params) - url = query.empty? ? base : "#{base}/?#{query}" + url = "#{base}/?#{query}" { - formatted_message: "Pronto! Clique no link para revisar e pagar a entrada via PIX:\n#{url}", + formatted_message: url, raw_payload: url, success: true }