fix(captain/mcp): generate_pix dispatcha fallback em mais cenários
Antes: fallback (link da página de reserva) só disparava quando Inter API
explodia (rescue StandardError). Erros estruturais (categoria não existe
na unit, período/dia inválido, sem credencial Inter cadastrada) caíam em
error_response e o LLM travava em "⏳ Um momento — vou verificar." sem
mandar nada pro cliente.
Agora: dispatch_no_pricing_fallback! cobre os casos onde Pix nem foi
tentado. Mesma UX do fallback existente — link pré-preenchido + label
pix_falhou_fallback pra triagem da gerência. Tool retorna isError=false
com instrução curta pro LLM ("só confirme que o link foi enviado, não
fale do problema técnico").
Caso real: Rodrigo pediu hidromassagem na inbox da Dolce Amore (unit que
não tem essa categoria). PricingTables retornou "Categoria não
reconhecida"; antes o LLM ficava no placeholder. Agora cliente recebe
link da página oficial automaticamente.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28e880d7b6
commit
bd494c424d
@ -79,7 +79,12 @@ class Captain::Mcp::Tools::GeneratePixTool < Captain::Mcp::Tools::BaseTool
|
|||||||
|
|
||||||
unit = resolve_unit(conversation, context)
|
unit = resolve_unit(conversation, context)
|
||||||
return error_response('Unidade do Captain não vinculada à inbox dessa conversa.') if unit.blank?
|
return error_response('Unidade do Captain não vinculada à inbox dessa conversa.') if unit.blank?
|
||||||
return error_response('Unidade não tem credenciais Inter configuradas. Avise a gerência.') unless unit.inter_credentials_present?
|
|
||||||
|
# Sem credencial Inter: vai DIRETO pro fallback de página de reserva ao
|
||||||
|
# invés de retornar erro pro LLM (que ele ia transformar em "vou
|
||||||
|
# verificar" e travar). Cliente recebe link da página oficial pra
|
||||||
|
# finalizar manualmente — UX uniforme.
|
||||||
|
return dispatch_no_pricing_fallback!(conversation, unit, args, 'inter_credentials_missing') unless unit.inter_credentials_present?
|
||||||
|
|
||||||
contact = conversation.contact
|
contact = conversation.contact
|
||||||
hydrate_contact_from_recent_messages!(contact, conversation)
|
hydrate_contact_from_recent_messages!(contact, conversation)
|
||||||
@ -92,7 +97,14 @@ class Captain::Mcp::Tools::GeneratePixTool < Captain::Mcp::Tools::BaseTool
|
|||||||
period: args['period'],
|
period: args['period'],
|
||||||
total_guests: (args['total_guests'] || 2).to_i
|
total_guests: (args['total_guests'] || 2).to_i
|
||||||
)
|
)
|
||||||
return error_response("Preço não calculado: #{pricing[:error]}") if pricing[:error].present?
|
# Erro estrutural (categoria não existe nessa unit, período inválido,
|
||||||
|
# dia indisponível). Antes retornava error_response e o LLM travava em
|
||||||
|
# "Um momento" sem mandar nada. Agora despacha link da página de
|
||||||
|
# reserva pra cliente concluir lá — UX consistente, marca pra triagem.
|
||||||
|
if pricing[:error].present?
|
||||||
|
Rails.logger.warn("[Captain::Mcp::GeneratePixTool] pricing inválido — usando fallback: #{pricing[:error]}")
|
||||||
|
return dispatch_no_pricing_fallback!(conversation, unit, args, "pricing: #{pricing[:error]}")
|
||||||
|
end
|
||||||
|
|
||||||
total_amount = pricing[:amount]
|
total_amount = pricing[:amount]
|
||||||
deposit = (total_amount * DEFAULT_DEPOSIT_RATIO).round(2)
|
deposit = (total_amount * DEFAULT_DEPOSIT_RATIO).round(2)
|
||||||
@ -310,6 +322,47 @@ class Captain::Mcp::Tools::GeneratePixTool < Captain::Mcp::Tools::BaseTool
|
|||||||
conversation.update_labels(merged)
|
conversation.update_labels(merged)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fallback "leve" pra cenários onde Pix nem foi tentado (categoria não
|
||||||
|
# existe na unit, período inválido, sem credencial Inter cadastrada).
|
||||||
|
# Sem reservation/pricing/valores — só monta link com o que tem do
|
||||||
|
# contact + args. UX é igual ao fallback de Inter falhar: cliente recebe
|
||||||
|
# link pra página oficial e conversa fica marcada pra triagem.
|
||||||
|
def dispatch_no_pricing_fallback!(conversation, unit, args, reason_code)
|
||||||
|
base = ENV.fetch('RESERVA_1001_BASE_URL',
|
||||||
|
InstallationConfig.find_by(name: 'RESERVA_1001_BASE_URL')&.value.presence ||
|
||||||
|
'https://reservas.hoteis1001noites.com.br')
|
||||||
|
contact = conversation.contact
|
||||||
|
custom = contact&.custom_attributes.to_h.with_indifferent_access
|
||||||
|
|
||||||
|
params = {
|
||||||
|
marca: unit&.brand&.name,
|
||||||
|
unidade: unit&.name,
|
||||||
|
permanencia: humanize_period(args['period'].to_s),
|
||||||
|
categoria: humanize_category(args['suite_category'].to_s),
|
||||||
|
checkin: parse_check_in(args['check_in_date'], conversation.account)&.iso8601,
|
||||||
|
nome: contact&.name,
|
||||||
|
telefone: contact&.phone_number,
|
||||||
|
cpf: custom[:cpf],
|
||||||
|
email: contact&.email
|
||||||
|
}.compact.reject { |_, v| v.to_s.strip.empty? }
|
||||||
|
|
||||||
|
url = "#{base.chomp('/')}/?#{URI.encode_www_form(params)}"
|
||||||
|
|
||||||
|
body = 'Pra evitar qualquer atrito no fechamento, é só finalizar pela página oficial ' \
|
||||||
|
"(seus dados já estão pré-preenchidos):\n#{url}"
|
||||||
|
|
||||||
|
Messages::MessageBuilder.new(nil, conversation, content: body, message_type: 'outgoing').perform
|
||||||
|
|
||||||
|
current = conversation.label_list
|
||||||
|
conversation.update_labels((current + %w[aguardando_pagamento pix_falhou_fallback]).uniq)
|
||||||
|
|
||||||
|
text_response(
|
||||||
|
"Pix indisponível (motivo=#{reason_code}). Mandei link da página de reserva pro cliente. " \
|
||||||
|
'Marquei conversa com pix_falhou_fallback pra gerência ver. NÃO repita o link nem fale sobre o problema técnico — ' \
|
||||||
|
'só confirme com o cliente que o link foi enviado.'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Quando a Inter API falha (auth, certificado, timeout, etc), em vez de
|
# Quando a Inter API falha (auth, certificado, timeout, etc), em vez de
|
||||||
# devolver erro, mandamos o cliente pra página oficial de reserva
|
# devolver erro, mandamos o cliente pra página oficial de reserva
|
||||||
# (reserva-1001) com query string preenchida. Cliente conclui por lá.
|
# (reserva-1001) com query string preenchida. Cliente conclui por lá.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user