diff --git a/db/migrate/20260502120000_create_captain_pricing_tables.rb b/db/migrate/20260502120000_create_captain_pricing_tables.rb new file mode 100644 index 000000000..fb48fb755 --- /dev/null +++ b/db/migrate/20260502120000_create_captain_pricing_tables.rb @@ -0,0 +1,29 @@ +class CreateCaptainPricingTables < ActiveRecord::Migration[7.1] + # rubocop:disable Metrics/MethodLength + def change + add_column :captain_units, :extra_person_fee, :decimal, precision: 10, scale: 2, default: 0.0, null: false + add_column :captain_units, :currency, :string, default: 'BRL', null: false + + create_table :captain_pricing_categories do |t| + t.references :captain_unit, null: false, foreign_key: { to_table: :captain_units } + t.string :key, null: false + t.jsonb :aliases, null: false, default: [] + t.integer :extra_person_starts_at, null: false, default: 3 + t.timestamps + end + add_index :captain_pricing_categories, [:captain_unit_id, :key], unique: true + + create_table :captain_pricing_amounts do |t| + t.references :captain_pricing_category, null: false, foreign_key: { to_table: :captain_pricing_categories } + t.string :period, null: false + t.string :day_bucket + t.decimal :amount, precision: 10, scale: 2, null: false + t.timestamps + end + add_index :captain_pricing_amounts, + [:captain_pricing_category_id, :period, :day_bucket], + unique: true, + name: 'idx_captain_pricing_amount_uniq' + end + # rubocop:enable Metrics/MethodLength +end diff --git a/db/migrate/20260502120100_add_provisioning_columns_to_captain_assistants.rb b/db/migrate/20260502120100_add_provisioning_columns_to_captain_assistants.rb new file mode 100644 index 000000000..661d2b607 --- /dev/null +++ b/db/migrate/20260502120100_add_provisioning_columns_to_captain_assistants.rb @@ -0,0 +1,14 @@ +class AddProvisioningColumnsToCaptainAssistants < ActiveRecord::Migration[7.1] + def change + add_column :captain_assistants, :hermes_subscription_secret, :string + add_column :captain_assistants, :hermes_port, :integer + add_column :captain_assistants, :parent_assistant_id, :bigint + + add_index :captain_assistants, :parent_assistant_id + add_index :captain_assistants, + :hermes_port, + unique: true, + where: 'hermes_port IS NOT NULL', + name: 'idx_captain_assistants_hermes_port_unique' + end +end diff --git a/db/seed_pricing_tables.rb b/db/seed_pricing_tables.rb new file mode 100644 index 000000000..a3de555c6 --- /dev/null +++ b/db/seed_pricing_tables.rb @@ -0,0 +1,150 @@ +# Backfill one-time das tabelas de preço pra Dolce Amore (unit 4) e +# Express (unit 5) — copia o que estava hardcoded no PricingTables.rb +# antes da migração pra DB. +# +# Idempotente: roda find_or_create_by em tudo. Pode rodar várias vezes sem +# criar duplicata. +# +# Uso: +# docker exec iachat_iachat_app bundle exec rails runner db/seed_pricing_tables.rb +# +# rubocop:disable all + +# Garante extra_person_fee + currency configurados nas units +dolce = Captain::Unit.find(4) +dolce.update!(extra_person_fee: 45.0, currency: 'BRL') if dolce.extra_person_fee.to_f.zero? + +express = Captain::Unit.find(5) +express.update!(extra_person_fee: 0.0, currency: 'BRL') + +DOLCE_AMORE_DATA = { + 'apartamento' => { + aliases: ['apto', 'standard', 'apartamento standard', 'apartamento_standard'], + extra_person_starts_at: 3, + prices: { '3h' => 85.0, 'pernoite_promo' => 110.0, 'pernoite_integral' => 155.0, 'diaria' => 290.0 } + }, + 'suite_master' => { + aliases: ['master', 'suite master', 'suíte master', '2 andares'], + extra_person_starts_at: 3, + prices: { '3h' => 90.0, 'pernoite_promo' => 130.0, 'pernoite_integral' => 180.0, 'diaria' => 340.0 } + }, + 'suite_luxo' => { + aliases: ['luxo', 'suite luxo', 'suíte luxo', 'classica', 'clássica'], + extra_person_starts_at: 3, + prices: { '3h' => 90.0, 'pernoite_promo' => 130.0, 'pernoite_integral' => 180.0, 'diaria' => 340.0 } + }, + 'suite_tematica' => { + aliases: ['tematica', 'temática', 'suite tematica', 'suíte temática'], + extra_person_starts_at: 3, + prices: { '3h' => 90.0, 'pernoite_promo' => 130.0, 'pernoite_integral' => 180.0, 'diaria' => 340.0 } + }, + 'mini_chale_45' => { + aliases: ['mini chale', 'mini chalé', 'chale 45', 'chalé 45', 'mini chalé 45', 'mini_chale'], + extra_person_starts_at: 3, + prices: { '3h' => 100.0, 'pernoite_promo' => 140.0, 'pernoite_integral' => 190.0, 'diaria' => 400.0 } + }, + 'chale_2_suites' => { + aliases: ['chale 2', 'chalé 2', 'chale 2 suites', 'chalé 2 suítes', 'chale_2', '2 suites'], + extra_person_starts_at: 4, + prices: { '3h' => 165.0, 'pernoite_promo' => 240.0, 'pernoite_integral' => 350.0, 'diaria' => 490.0 } + }, + 'suite_ouro' => { + aliases: ['ouro', 'suite ouro', 'suíte ouro'], + extra_person_starts_at: 4, + prices: { '3h' => 230.0, 'pernoite_promo' => 340.0, 'pernoite_integral' => 440.0, 'diaria' => 830.0 } + }, + 'chale_master_4_suites' => { + aliases: ['chale master', 'chalé master', 'master 4 suites', 'chalé master 4 suítes', 'chale_master', '4 suites'], + extra_person_starts_at: 8, + prices: { '3h' => 360.0, 'pernoite_promo' => 510.0, 'pernoite_integral' => 580.0, 'diaria' => 1240.0 } + } +}.freeze + +EXPRESS_DATA = { + 'standard' => { + aliases: %w[standard comum básica basica apartamento\ standard], + extra_person_starts_at: 3, + prices: { + '2h' => { 'mon_wed' => 40.0, 'thu_sun' => 50.0 }, + '3h' => { 'mon_wed' => 50.0, 'thu_sun' => 65.0 }, + '4h' => { 'mon_wed' => 60.0, 'thu_sun' => 80.0 }, + 'pernoite_promo' => { 'mon_wed' => 100.0, 'thu_sun' => 120.0 }, + 'diaria' => 150.0 + } + }, + 'master' => { + aliases: ['master', 'melhor', 'suite master', 'suíte master'], + extra_person_starts_at: 3, + prices: { + '2h' => { 'mon_wed' => 50.0, 'thu_sun' => 60.0 }, + '3h' => { 'mon_wed' => 60.0, 'thu_sun' => 75.0 }, + '4h' => { 'mon_wed' => 70.0 }, + '5h' => { 'thu_sun' => 85.0 }, + 'pernoite_promo' => { 'mon_wed' => 120.0, 'thu_sun' => 140.0 }, + 'diaria' => 160.0 + } + }, + 'singles' => { + aliases: %w[singles single sozinho], + extra_person_starts_at: 99, + prices: { + 'pernoite_promo' => { 'mon_wed' => 80.0, 'thu_sun' => 110.0 }, + 'diaria' => 130.0 + } + }, + 'familia' => { + aliases: %w[familia família familiar], + extra_person_starts_at: 99, + prices: { + 'pernoite_promo' => 160.0, + 'diaria' => 190.0 + } + }, + 'singles_duplo' => { + aliases: ['singles duplo', 'singles_duplo', 'casal', 'duplo'], + extra_person_starts_at: 99, + prices: { + 'pernoite_promo' => { 'mon_wed' => 180.0, 'thu_sun' => 220.0 }, + 'diaria' => 250.0 + } + } +}.freeze + +def upsert(unit, data) + data.each do |key, attrs| + cat = Captain::PricingCategory.find_or_initialize_by(captain_unit_id: unit.id, key: key) + cat.aliases = attrs[:aliases] + cat.extra_person_starts_at = attrs[:extra_person_starts_at] + cat.save! + + attrs[:prices].each do |period, value| + if value.is_a?(Hash) + value.each do |bucket, amount| + row = Captain::PricingAmount.find_or_initialize_by( + captain_pricing_category_id: cat.id, period: period, day_bucket: bucket + ) + row.amount = amount + row.save! + end + else + row = Captain::PricingAmount.find_or_initialize_by( + captain_pricing_category_id: cat.id, period: period, day_bucket: nil + ) + row.amount = value + row.save! + end + end + + puts "✓ unit=#{unit.id} #{key} (#{attrs[:prices].size} periods)" + end +end + +upsert(dolce, DOLCE_AMORE_DATA) +upsert(express, EXPRESS_DATA) + +puts "--- summary ---" +puts "dolce categories: #{dolce.pricing_categories.count}" +puts "dolce amounts: #{Captain::PricingAmount.joins(:pricing_category).where(captain_pricing_categories: { captain_unit_id: dolce.id }).count}" +puts "express categories: #{express.pricing_categories.count}" +puts "express amounts: #{Captain::PricingAmount.joins(:pricing_category).where(captain_pricing_categories: { captain_unit_id: express.id }).count}" +# rubocop:enable all diff --git a/enterprise/app/models/captain/pricing_amount.rb b/enterprise/app/models/captain/pricing_amount.rb new file mode 100644 index 000000000..8b895bca4 --- /dev/null +++ b/enterprise/app/models/captain/pricing_amount.rb @@ -0,0 +1,40 @@ +# == Schema Information +# +# Table name: captain_pricing_amounts +# +# id :bigint not null, primary key +# amount :decimal(10, 2) not null +# day_bucket :string +# period :string not null +# created_at :datetime not null +# updated_at :datetime not null +# captain_pricing_category_id :bigint not null +# +# Indexes +# +# idx_captain_pricing_amount_uniq (captain_pricing_category_id,period,day_bucket) UNIQUE +# index_captain_pricing_amounts_on_captain_pricing_category_id (captain_pricing_category_id) +# +# Foreign Keys +# +# fk_rails_... (captain_pricing_category_id => captain_pricing_categories.id) +# +# Valor por (categoria, período, dia da semana). day_bucket NULL = preço +# único todos os dias. day_bucket='mon_wed' = seg-qua. 'thu_sun' = qui-dom. +class Captain::PricingAmount < ApplicationRecord + self.table_name = 'captain_pricing_amounts' + + PERIODS = %w[2h 3h 4h 5h pernoite_promo pernoite_integral diaria].freeze + DAY_BUCKETS = %w[mon_wed thu_sun].freeze + + belongs_to :pricing_category, + class_name: 'Captain::PricingCategory', + foreign_key: :captain_pricing_category_id, + inverse_of: :amounts + + validates :period, inclusion: { in: PERIODS } + validates :day_bucket, inclusion: { in: DAY_BUCKETS, allow_nil: true } + validates :amount, numericality: { greater_than: 0 } + validates :captain_pricing_category_id, + uniqueness: { scope: %i[period day_bucket] } +end diff --git a/enterprise/app/models/captain/pricing_category.rb b/enterprise/app/models/captain/pricing_category.rb new file mode 100644 index 000000000..37c514c7d --- /dev/null +++ b/enterprise/app/models/captain/pricing_category.rb @@ -0,0 +1,55 @@ +# == Schema Information +# +# Table name: captain_pricing_categories +# +# id :bigint not null, primary key +# aliases :jsonb not null +# extra_person_starts_at :integer default(3), not null +# key :string not null +# created_at :datetime not null +# updated_at :datetime not null +# captain_unit_id :bigint not null +# +# Indexes +# +# index_captain_pricing_categories_on_captain_unit_id (captain_unit_id) +# index_captain_pricing_categories_on_captain_unit_id_and_key (captain_unit_id,key) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (captain_unit_id => captain_units.id) +# +# Categoria de preço por unidade — espelha o que tava em PricingTables.rb +# como Ruby hash. `key` é canônico ('apartamento', 'standard', etc), +# `aliases` é array de strings que o LLM pode digitar e bate via fuzzy +# match. `extra_person_starts_at` controla a partir de qual hóspede a taxa +# extra começa (default 3 = casal incluso). +class Captain::PricingCategory < ApplicationRecord + self.table_name = 'captain_pricing_categories' + + belongs_to :captain_unit, class_name: 'Captain::Unit', inverse_of: :pricing_categories + has_many :amounts, + class_name: 'Captain::PricingAmount', + foreign_key: :captain_pricing_category_id, + inverse_of: :pricing_category, + dependent: :destroy + + validates :key, presence: true, uniqueness: { scope: :captain_unit_id } + validates :extra_person_starts_at, numericality: { greater_than_or_equal_to: 1 } + validate :aliases_is_array + + def matches?(needle) + return false if needle.blank? + + candidates = ([key.to_s.tr('_', ' ')] + aliases.to_a).map { |s| s.to_s.downcase.strip } + candidates.include?(needle.to_s.downcase.strip.tr('_', ' ').squeeze(' ')) + end + + private + + def aliases_is_array + return if aliases.is_a?(Array) + + errors.add(:aliases, 'must be an array') + end +end diff --git a/enterprise/app/models/captain/unit.rb b/enterprise/app/models/captain/unit.rb index 2fc36f2b1..1b1f6a6ad 100644 --- a/enterprise/app/models/captain/unit.rb +++ b/enterprise/app/models/captain/unit.rb @@ -63,6 +63,8 @@ class Captain::Unit < ApplicationRecord 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 diff --git a/enterprise/app/services/captain/mcp/pricing_tables.rb b/enterprise/app/services/captain/mcp/pricing_tables.rb index aa584eddf..70d2efac4 100644 --- a/enterprise/app/services/captain/mcp/pricing_tables.rb +++ b/enterprise/app/services/captain/mcp/pricing_tables.rb @@ -1,209 +1,86 @@ # Tabelas de preço por unidade do Captain — fonte de verdade backend pra -# tools MCP que precisam validar valor. Espelha o que está nos prompts/skills -# das assistentes (Valentina, Jasmines, etc), mas centralizada e auditável. +# tools MCP que precisam validar valor. # # Quando o LLM chama `generate_pix`, ele NÃO informa o valor; apenas -# categoria/período. Tool calcula via essa tabela. Isso impede que o LLM -# invente um valor (ex: "aplicou desconto VIP" sozinho). +# categoria/período/data. Tool calcula via essa tabela no banco. Isso +# impede que o LLM invente um valor. # -# Estrutura: TABLES[captain_unit_id] = { -# categories: { -# '' => { -# # Preço por período. Aceita 2 formatos: -# # 1) Valor único (motel sem variação por dia da semana): -# # prices: { '3h' => 85.0, 'pernoite_promo' => 110.0 } -# # 2) Hash por bucket de dia da semana (hotel: qui-dom caro): -# # prices: { '3h' => { 'mon_wed' => 50.0, 'thu_sun' => 65.0 } } -# # Buckets aceitos: 'mon_wed' (seg-qua) e 'thu_sun' (qui-dom). -# prices: { '3h' => 85, ... }, -# aliases: ['apto', 'standard', ...] -# } -# }, -# extra_person_fee: 45, -# extra_person_rules: { '' => starts_at_guest_n } -# } -# rubocop:disable Metrics/ModuleLength +# Fonte de dados: tabelas Captain::PricingCategory + Captain::PricingAmount, +# escritas pelo admin via UI ou pelo Construtor via script `hermes-provision`. +# Antes de v147 esses dados viviam num arquivo Ruby hardcoded — migrado pra +# DB pra permitir cadastro dinâmico de unidades sem deploy. module Captain::Mcp::PricingTables PERIOD_KEYS = %w[2h 3h 4h 5h pernoite_promo pernoite_integral diaria].freeze - # mon_wed cobre wday 1,2,3 (seg-qua); thu_sun cobre wday 4,5,6,0 (qui-dom). DAY_BUCKETS = %w[mon_wed thu_sun].freeze DEFAULT_TZ = 'America/Sao_Paulo'.freeze - TABLES = { - # Motel Dolce Amore — Ponta Negra, Natal/RN (captain_unit_id=4) - 4 => { - currency: 'BRL', - extra_person_fee: 45.0, - # Por categoria, a partir de qual hóspede a taxa começa a contar. - # Ex: "starts_at_guest_n=3" significa que 3ª pessoa em diante paga. - # Default 3 — base do quarto inclui 2 pessoas (casal). - categories: { - 'apartamento' => { - prices: { '3h' => 85.0, 'pernoite_promo' => 110.0, 'pernoite_integral' => 155.0, 'diaria' => 290.0 }, - extra_person_starts_at: 3, - aliases: ['apto', 'standard', 'apartamento standard', 'apartamento_standard'] - }, - 'suite_master' => { - prices: { '3h' => 90.0, 'pernoite_promo' => 130.0, 'pernoite_integral' => 180.0, 'diaria' => 340.0 }, - extra_person_starts_at: 3, - aliases: ['master', 'suite master', 'suíte master', '2 andares'] - }, - 'suite_luxo' => { - prices: { '3h' => 90.0, 'pernoite_promo' => 130.0, 'pernoite_integral' => 180.0, 'diaria' => 340.0 }, - extra_person_starts_at: 3, - aliases: ['luxo', 'suite luxo', 'suíte luxo', 'classica', 'clássica'] - }, - 'suite_tematica' => { - prices: { '3h' => 90.0, 'pernoite_promo' => 130.0, 'pernoite_integral' => 180.0, 'diaria' => 340.0 }, - extra_person_starts_at: 3, - aliases: ['tematica', 'temática', 'suite tematica', 'suíte temática'] - }, - 'mini_chale_45' => { - prices: { '3h' => 100.0, 'pernoite_promo' => 140.0, 'pernoite_integral' => 190.0, 'diaria' => 400.0 }, - extra_person_starts_at: 3, - aliases: ['mini chale', 'mini chalé', 'chale 45', 'chalé 45', 'mini chalé 45', 'mini_chale'] - }, - 'chale_2_suites' => { - prices: { '3h' => 165.0, 'pernoite_promo' => 240.0, 'pernoite_integral' => 350.0, 'diaria' => 490.0 }, - extra_person_starts_at: 4, - aliases: ['chale 2', 'chalé 2', 'chale 2 suites', 'chalé 2 suítes', 'chale_2', '2 suites'] - }, - 'suite_ouro' => { - prices: { '3h' => 230.0, 'pernoite_promo' => 340.0, 'pernoite_integral' => 440.0, 'diaria' => 830.0 }, - extra_person_starts_at: 4, - aliases: ['ouro', 'suite ouro', 'suíte ouro'] - }, - 'chale_master_4_suites' => { - prices: { '3h' => 360.0, 'pernoite_promo' => 510.0, 'pernoite_integral' => 580.0, 'diaria' => 1240.0 }, - extra_person_starts_at: 8, - aliases: ['chale master', 'chalé master', 'master 4 suites', 'chalé master 4 suítes', 'chale_master', '4 suites'] - } - } - }, - # Hotel 1001 Noites Express — Águas Lindas/GO (captain_unit_id=5) - # Preço varia por dia da semana (mon_wed = seg-qua / thu_sun = qui-dom). - # Diária e Família são flat (mesmo preço todos os dias). - 5 => { - currency: 'BRL', - extra_person_fee: 0.0, - categories: { - 'standard' => { - prices: { - '2h' => { 'mon_wed' => 40.0, 'thu_sun' => 50.0 }, - '3h' => { 'mon_wed' => 50.0, 'thu_sun' => 65.0 }, - '4h' => { 'mon_wed' => 60.0, 'thu_sun' => 80.0 }, - 'pernoite_promo' => { 'mon_wed' => 100.0, 'thu_sun' => 120.0 }, - 'diaria' => 150.0 - }, - extra_person_starts_at: 3, - aliases: ['standard', 'comum', 'básica', 'basica', 'apartamento standard'] - }, - 'master' => { - prices: { - '2h' => { 'mon_wed' => 50.0, 'thu_sun' => 60.0 }, - '3h' => { 'mon_wed' => 60.0, 'thu_sun' => 75.0 }, - '4h' => { 'mon_wed' => 70.0 }, - '5h' => { 'thu_sun' => 85.0 }, - 'pernoite_promo' => { 'mon_wed' => 120.0, 'thu_sun' => 140.0 }, - 'diaria' => 160.0 - }, - extra_person_starts_at: 3, - aliases: ['master', 'melhor', 'suite master', 'suíte master'] - }, - 'singles' => { - prices: { - 'pernoite_promo' => { 'mon_wed' => 80.0, 'thu_sun' => 110.0 }, - 'diaria' => 130.0 - }, - extra_person_starts_at: 99, - aliases: %w[singles single sozinho] - }, - 'familia' => { - prices: { - 'pernoite_promo' => 160.0, - 'diaria' => 190.0 - }, - extra_person_starts_at: 99, - aliases: %w[familia família familiar] - }, - 'singles_duplo' => { - prices: { - 'pernoite_promo' => { 'mon_wed' => 180.0, 'thu_sun' => 220.0 }, - 'diaria' => 250.0 - }, - extra_person_starts_at: 99, - aliases: ['singles duplo', 'singles_duplo', 'casal', 'duplo'] - } - } - } - }.freeze - class << self # Retorna {amount:, breakdown:} ou erro {error:} pra uma cobrança. - # period: '3h' | 'pernoite_promo' | 'pernoite_integral' | 'diaria' - # check_in_at: Time/String ISO8601. Determina o bucket de dia da semana - # quando o preço varia (mon_wed/thu_sun). Default: agora. - # total_guests: número TOTAL de hóspedes — a função calcula extras - # baseado em extra_person_starts_at. # rubocop:disable Metrics/MethodLength,Metrics/AbcSize def calculate(unit_id:, suite_category:, period:, total_guests: 2, check_in_at: nil) - table = TABLES[unit_id] - return { error: "Unidade #{unit_id} não tem tabela de preços cadastrada." } if table.blank? + unit = Captain::Unit.find_by(id: unit_id) + return { error: "Unidade #{unit_id} não cadastrada." } if unit.blank? - cat_key, cat_data = find_category(table, suite_category) - return { error: "Categoria '#{suite_category}' não reconhecida nesta unidade." } if cat_data.blank? + category = find_category(unit, suite_category) + return { error: "Categoria '#{suite_category}' não reconhecida nesta unidade." } if category.blank? period_key = normalize_period(period) return { error: "Período '#{period}' inválido. Use: #{PERIOD_KEYS.join(', ')}." } if period_key.blank? - raw = cat_data[:prices][period_key] - return { error: "Preço de '#{period_key}' não definido para '#{cat_key}'." } if raw.blank? - day_bucket = resolve_day_bucket(check_in_at) - base, used_bucket = resolve_price(raw, day_bucket, period_key, cat_key) + base, used_bucket = resolve_amount(category, period_key, day_bucket) return { error: base } if base.is_a?(String) - starts_at = cat_data[:extra_person_starts_at] || 3 + starts_at = category.extra_person_starts_at || 3 extra_guests = [total_guests.to_i - (starts_at - 1), 0].max - extra_total = extra_guests * table[:extra_person_fee] + extra_total = extra_guests * unit.extra_person_fee.to_f total = (base + extra_total).round(2) { amount: total, breakdown: { - unit_id: unit_id, - suite_category: cat_key, + unit_id: unit.id, + suite_category: category.key, period: period_key, day_bucket: used_bucket, base_price: base, total_guests: total_guests, extra_guests: extra_guests, - extra_person_fee: table[:extra_person_fee], + extra_person_fee: unit.extra_person_fee.to_f, extra_total: extra_total } } end + # rubocop:enable Metrics/MethodLength,Metrics/AbcSize def categories_for(unit_id) - TABLES.dig(unit_id, :categories)&.keys || [] + Captain::PricingCategory.where(captain_unit_id: unit_id).order(:key).pluck(:key) end private - # Recebe Numeric (preço único) ou Hash{bucket=>preço}. Retorna [valor, bucket] - # ou [erro_string, nil] se o bucket pedido não tiver preço cadastrado. - def resolve_price(raw, day_bucket, period_key, cat_key) - return [raw.to_f, nil] if raw.is_a?(Numeric) + def find_category(unit, raw) + return nil if raw.blank? - return ["Estrutura de preço inválida pra '#{cat_key}/#{period_key}'.", nil] unless raw.is_a?(Hash) + needle = raw.to_s.downcase.strip.tr('_', ' ').squeeze(' ') + unit.pricing_categories.find { |cat| cat.matches?(needle) } + end - price = raw[day_bucket] || raw[day_bucket.to_s] - if price.blank? - avail = raw.keys.map(&:to_s).join(', ') - return ["'#{cat_key}/#{period_key}' não tem preço pro dia escolhido (#{day_bucket}). Disponível: #{avail}.", nil] - end + def resolve_amount(category, period_key, day_bucket) + amounts = category.amounts.where(period: period_key) + return ["Preço de '#{period_key}' não definido para '#{category.key}'.", nil] if amounts.empty? - [price.to_f, day_bucket] + # 1) Tenta match exato no day_bucket + hit = amounts.find_by(day_bucket: day_bucket) + return [hit.amount.to_f, day_bucket] if hit + + # 2) Tenta preço flat (day_bucket NULL) + hit = amounts.find_by(day_bucket: nil) + return [hit.amount.to_f, nil] if hit + + # 3) Bucket pedido não tem amount + não é flat + avail = amounts.pluck(:day_bucket).map(&:to_s).reject(&:blank?).uniq.join(', ').presence || 'flat' + ["'#{category.key}/#{period_key}' não tem preço pro dia escolhido (#{day_bucket}). Disponível: #{avail}.", nil] end # mon_wed: wday 1,2,3 (seg, ter, qua) @@ -219,23 +96,10 @@ module Captain::Mcp::PricingTables [1, 2, 3].include?(time.wday) ? 'mon_wed' : 'thu_sun' end - def find_category(table, raw) - needle = raw.to_s.downcase.strip.tr('_', ' ').squeeze(' ') - return [nil, nil] if needle.blank? - - table[:categories].each do |key, data| - candidates = ([key.tr('_', ' ')] + data[:aliases].to_a).map { |c| c.to_s.downcase.strip } - return [key, data] if candidates.any?(needle) - end - - [nil, nil] - end - def normalize_period(raw) key = raw.to_s.downcase.strip.tr('-', '_') return key if PERIOD_KEYS.include?(key) - # aceita variações comuns case key when 'pernoite', 'pernoite_normal', 'promocional' then 'pernoite_promo' when 'feriado', 'pernoite_feriado', 'sex_sab', 'final_de_semana' then 'pernoite_integral' @@ -243,7 +107,5 @@ module Captain::Mcp::PricingTables when 'diária' then 'diaria' end end - # rubocop:enable Metrics/MethodLength,Metrics/AbcSize end end -# rubocop:enable Metrics/ModuleLength