iachat/enterprise/app/services/captain/payments/confirmation_service.rb
Rodribm10 e94cadbdf6 feat(captain): pix_mode manual_static pra Padova e Express
Adiciona caminho paralelo de PIX manual estático pra unidades sem
integração Inter (Padova, Express AL). Mudança 100% aditiva — todas as
outras unidades continuam no fluxo Inter inalterado (default
pix_mode=inter_dynamic aplicado pela migration).

Backend (sem SOUL/SKILL ainda — Frente 7 vem depois):
- Migration concurrent: pix_mode + 4 campos manual_pix_* em captain_units;
  provider + manual_proof_payload + manual_review_reason em captain_pix_charges
- Captain::Unit: enum pix_mode (prefix), validação condicional manual_*
- Captain::PixCharge: status estendido (awaiting_proof, pending_review),
  scope manual/inter, nota interna ramificada por modo
- GeneratePixTool MCP: branch manual_static (chave fixa, mensagem direta
  sem QR/Inter), preserva fluxo Inter intacto
- 4 tools MCP novas: verificar_comprovante_pix (vision gpt-5.3-codex),
  criar_nota_interna (genérica), confirmar_reserva_pix_manual (wrapper
  do ConfirmationService), marcar_reserva_pendente
- ConfirmationService: source_label cobre 'manual_pix_proof'

Próximos passos manuais (não inclusos neste commit):
1. Rodar migration em prod (entrypoint não roda no boot)
2. Seed Padova/Express com pix_mode=manual_static + chaves Stone
3. Deploy nova imagem via docker service update
4. Editar SOUL/SKILL Padova/Express na VPS Hermes + kill+boot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:01:01 -03:00

105 lines
3.6 KiB
Ruby

# frozen_string_literal: true
# Serviço para confirmar pagamento de uma reserva
# Atualiza status, labels e cria nota interna
class Captain::Payments::ConfirmationService
def initialize(reservation:, source:, payload: nil, actor: nil)
@reservation = reservation
@source = source.to_s
@payload = payload
@actor = actor
end
def perform
was_already_paid = reservation.payment_status.to_s == 'paid'
ActiveRecord::Base.transaction do
mark_reservation_paid!
sync_conversation_labels!
create_internal_note_once!
end
enqueue_roulette_offer! unless was_already_paid
notify_hermes_proactively! unless was_already_paid
Rails.logger.info "[PaymentConfirmation] Reserva #{@reservation.id} confirmada (#{source_label})"
end
private
attr_reader :reservation, :source, :payload, :actor
def mark_reservation_paid!
attrs = { payment_status: :paid }
attrs[:status] = :active if reservation.respond_to?(:active?) && !reservation.active?
reservation.update!(attrs)
end
def sync_conversation_labels!
conversation = reservation.conversation
return if conversation.blank?
current = conversation.label_list
merged = (current + %w[pagamento_confirmado reserva_feita]).uniq
merged -= %w[aguardando_pagamento comprovante_recebido pagamento_em_revisao]
conversation.update_labels(merged)
end
def create_internal_note_once!
conversation = reservation.conversation
return if conversation.blank?
return if confirmation_note_already_created?
content = [
"💰 Pagamento confirmado automaticamente (#{source_label}).",
"📋 Reserva ##{reservation.id}",
("🔗 Origem: #{source}" if source.present?)
].compact.join("\n")
Messages::MessageBuilder.new(actor, conversation, { content: content, private: true }).perform
mark_note_created!
end
def source_label
case source
when 'webhook_inter_pix' then 'webhook Inter Pix'
when 'payment_callback' then 'callback de pagamento'
when 'inter_cob_query_polling' then 'consulta periódica no Inter'
when 'inter_cob_query' then 'consulta manual no Inter'
when 'manual_pix_proof' then 'comprovante PIX manual validado'
else
'integração de pagamento'
end
end
def confirmation_note_already_created?
reservation.metadata.to_h['payment_confirmed_note_at'].present?
end
def mark_note_created!
metadata = reservation.metadata.to_h
metadata['payment_confirmed_note_at'] ||= Time.current.iso8601
metadata['payment_confirmed_source'] ||= source
metadata['payment_confirmed_payload'] ||= payload if payload.present?
reservation.update_column(:metadata, metadata)
end
# Dispara a oferta da Roleta da Sorte após confirmação.
# Fora da transação — roleta é side effect; se falhar, confirmação continua válida.
def enqueue_roulette_offer!
Captain::Payments::OfferRouletteJob.perform_later(reservation.id)
rescue StandardError => e
Rails.logger.warn("[PaymentConfirmation] falha ao enfileirar roleta reserva=#{reservation.id}: #{e.class} - #{e.message}")
end
# Notifica o Hermes Agent (se a inbox estiver no fluxo Hermes) pra mandar
# mensagem espontânea pro cliente. Coexiste com o fluxo Captain interno —
# se a inbox NÃO estiver no Hermes, o job ignora silenciosamente. Side
# effect: nunca bloqueia a confirmação.
def notify_hermes_proactively!
Captain::Hermes::NotifyPaymentConfirmedJob.perform_later(reservation.id)
rescue StandardError => e
Rails.logger.warn("[PaymentConfirmation] falha ao notificar Hermes reserva=#{reservation.id}: #{e.class} - #{e.message}")
end
end