# == Schema Information # # Table name: captain_units # # id :bigint not null, primary key # concierge_config :jsonb not null # currency :string default("BRL"), not null # extra_person_fee :decimal(10, 2) default(0.0), not null # inter_account_number :string # inter_cert_content :text # inter_cert_path :string # inter_client_secret :string # inter_key_content :text # inter_key_path :string # inter_pix_key :string # last_synced_at :datetime # leader_whatsapp :string # name :string not null # payment_receipt_review_enabled :boolean default(FALSE), not null # plug_play_token :string # proactive_pix_polling_enabled :boolean default(FALSE), not null # reservation_source_tag :string # reservations_sync_enabled :boolean # status :string # suite_category_images :jsonb not null # visible_suite_categories :jsonb not null # webhook_configured_at :datetime # webhook_url :string # created_at :datetime not null # updated_at :datetime not null # account_id :bigint not null # captain_brand_id :bigint not null # concierge_inbox_id :bigint # inbox_id :bigint # inter_client_id :string # plug_play_id :string # supabase_marca_id :uuid # supabase_tenant_id :bigint default(1) # supabase_unit_id :uuid # # Indexes # # index_captain_units_on_account_id (account_id) # index_captain_units_on_captain_brand_id (captain_brand_id) # index_captain_units_on_concierge_inbox_id (concierge_inbox_id) # index_captain_units_on_inbox_id (inbox_id) # index_captain_units_on_supabase_unit_id (supabase_unit_id) UNIQUE WHERE (supabase_unit_id IS NOT NULL) # # Foreign Keys # # fk_rails_... (account_id => accounts.id) # fk_rails_... (captain_brand_id => captain_brands.id) # fk_rails_... (concierge_inbox_id => inboxes.id) # fk_rails_... (inbox_id => inboxes.id) # class Captain::Unit < ApplicationRecord self.table_name = 'captain_units' belongs_to :account belongs_to :brand, class_name: 'Captain::Brand', foreign_key: 'captain_brand_id', inverse_of: :units belongs_to :concierge_inbox, class_name: 'Inbox', optional: true has_many :unit_inboxes, class_name: 'Captain::UnitInbox', foreign_key: :captain_unit_id, inverse_of: :captain_unit, dependent: :destroy has_many :inboxes, through: :unit_inboxes has_many :pix_charges, class_name: 'Captain::PixCharge', dependent: :restrict_with_error has_many :gallery_items, class_name: 'Captain::GalleryItem', foreign_key: :captain_unit_id, inverse_of: :captain_unit, dependent: :destroy has_many :pricing_categories, class_name: 'Captain::PricingCategory', foreign_key: :captain_unit_id, inverse_of: :captain_unit, dependent: :destroy encrypts :inter_client_secret encrypts :inter_account_number encrypts :inter_cert_content encrypts :inter_key_content enum status: { active: 'active', inactive: 'inactive' }, _default: 'active' enum pix_mode: { inter_dynamic: 'inter_dynamic', manual_static: 'manual_static' }, _default: 'inter_dynamic', _prefix: true MANUAL_PIX_KEY_TYPES = %w[cpf cnpj email phone random].freeze validates :name, presence: true validates :manual_pix_key_type, inclusion: { in: MANUAL_PIX_KEY_TYPES }, allow_nil: true validate :proactive_pix_polling_requires_inter_credentials validate :manual_static_requires_manual_pix_fields after_commit :enqueue_supabase_provisioning, on: :create def concierge_persona_name concierge_config_hash['persona_name'].presence || 'Sofia' end def concierge_knowledge concierge_config_hash['knowledge'].to_s end def concierge_variables concierge_config_hash['variables'].to_h end def concierge_configured? concierge_inbox_id.present? end def inter_credentials_present? inter_client_id.present? && inter_client_secret.present? && inter_pix_key.present? && (inter_cert_content.present? || resolved_inter_cert_path.present?) && (inter_key_content.present? || resolved_inter_key_path.present?) end def manual_pix_configured? pix_mode_manual_static? && manual_pix_key.present? && manual_pix_owner_name.present? && manual_pix_bank_name.present? end def resolved_inter_cert_path resolve_certificate_path(inter_cert_path) end def resolved_inter_key_path resolve_certificate_path(inter_key_path) end private def concierge_config_hash (concierge_config || {}).with_indifferent_access end def proactive_pix_polling_requires_inter_credentials return unless proactive_pix_polling_enabled? return if inter_credentials_present? errors.add( :proactive_pix_polling_enabled, 'só pode ser habilitado quando a integração Inter estiver completa (client id/secret, chave pix, cert e key)' ) end def manual_static_requires_manual_pix_fields return unless pix_mode_manual_static? %i[manual_pix_key manual_pix_owner_name manual_pix_bank_name].each do |field| errors.add(field, 'é obrigatório quando pix_mode = manual_static') if public_send(field).blank? end end # Resolve o path do certificado — suporta caminho absoluto, relativo ao Rails.root # ou nome de arquivo simples dentro de storage/certs/. def resolve_certificate_path(path) return nil if path.blank? return path if File.exist?(path) rails_root_path = Rails.root.join(path).to_s return rails_root_path if File.exist?(rails_root_path) filename = File.basename(path) fallback_path = Rails.root.join('storage/certs', filename).to_s return fallback_path if File.exist?(fallback_path) path # Retorna original se nenhum caminho for encontrado end def enqueue_supabase_provisioning Captain::Reserva::ProvisionUnitInSupabaseJob.perform_later(id) rescue StandardError => e Rails.logger.warn("[Captain::Unit##{id}] enqueue ProvisionUnitInSupabaseJob falhou: #{e.class} - #{e.message}") end end