feat(lifecycle): add Captain::Lifecycle::Rule model with filter matching
TDD: 16 examples passing. Adds EVENTS constant, active/for_event scopes, and matches_reservation? with unit_ids/categorias/permanencias filters. Also adds captain_reservation factory used by the spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ee3fcd4ef
commit
ffc5ac7fb8
61
enterprise/app/models/captain/lifecycle/rule.rb
Normal file
61
enterprise/app/models/captain/lifecycle/rule.rb
Normal file
@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Captain::Lifecycle::Rule < ApplicationRecord
|
||||
self.table_name = 'captain_lifecycle_rules'
|
||||
|
||||
EVENTS = %w[
|
||||
reservation.confirmed
|
||||
checkin.scheduled_at
|
||||
checkin.detected
|
||||
checkout.scheduled_at
|
||||
checkout.detected
|
||||
reservation.cancelled
|
||||
reservation.no_show
|
||||
].freeze
|
||||
|
||||
MESSAGE_TYPES = %w[text buttons list url_button].freeze
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates :event, presence: true, inclusion: { in: EVENTS }
|
||||
validates :message_body, presence: true
|
||||
validates :message_type, inclusion: { in: MESSAGE_TYPES }
|
||||
|
||||
scope :active, -> { where(enabled: true) }
|
||||
scope :for_event, ->(event) { where(event: event) }
|
||||
|
||||
def matches_reservation?(reservation)
|
||||
return false unless reservation
|
||||
|
||||
filters_hash = filters.presence || {}
|
||||
matches_unit?(filters_hash, reservation) &&
|
||||
matches_categoria?(filters_hash, reservation) &&
|
||||
matches_permanencia?(filters_hash, reservation)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def matches_unit?(filters_hash, reservation)
|
||||
unit_ids = Array(filters_hash['unit_ids'])
|
||||
return true if unit_ids.empty?
|
||||
|
||||
unit_ids.include?(reservation.captain_unit_id)
|
||||
end
|
||||
|
||||
def matches_categoria?(filters_hash, reservation)
|
||||
categorias = Array(filters_hash['categorias'])
|
||||
return true if categorias.empty?
|
||||
|
||||
categorias.include?(reservation.suite_identifier)
|
||||
end
|
||||
|
||||
def matches_permanencia?(filters_hash, reservation)
|
||||
permanencias = Array(filters_hash['permanencias'])
|
||||
return true if permanencias.empty?
|
||||
|
||||
actual = reservation.metadata.to_h['permanencia']
|
||||
permanencias.include?(actual)
|
||||
end
|
||||
end
|
||||
85
spec/enterprise/models/captain/lifecycle/rule_spec.rb
Normal file
85
spec/enterprise/models/captain/lifecycle/rule_spec.rb
Normal file
@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Lifecycle::Rule, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:account) }
|
||||
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
subject { build(:captain_lifecycle_rule) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_presence_of(:event) }
|
||||
it { is_expected.to validate_presence_of(:message_body) }
|
||||
it { is_expected.to validate_inclusion_of(:event).in_array(Captain::Lifecycle::Rule::EVENTS) }
|
||||
it { is_expected.to validate_inclusion_of(:message_type).in_array(%w[text buttons list url_button]) }
|
||||
end
|
||||
|
||||
describe '.active' do
|
||||
it 'returns only enabled rules' do
|
||||
enabled = create(:captain_lifecycle_rule, enabled: true)
|
||||
create(:captain_lifecycle_rule, enabled: false)
|
||||
expect(described_class.active).to contain_exactly(enabled)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_event' do
|
||||
it 'filters by event' do
|
||||
rule = create(:captain_lifecycle_rule, event: 'checkin.scheduled_at')
|
||||
create(:captain_lifecycle_rule, event: 'reservation.confirmed')
|
||||
expect(described_class.for_event('checkin.scheduled_at')).to contain_exactly(rule)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#matches_reservation?' do
|
||||
let(:account) { create(:account) }
|
||||
let(:brand) { Captain::Brand.create!(account: account, name: 'Brand Test') }
|
||||
let(:unit) { Captain::Unit.create!(account: account, captain_brand_id: brand.id, name: 'Águas Lindas') }
|
||||
let(:reservation) do
|
||||
create(:captain_reservation,
|
||||
account: account,
|
||||
unit: unit,
|
||||
suite_identifier: 'Alexa',
|
||||
metadata: { 'permanencia' => 'Pernoite' })
|
||||
end
|
||||
|
||||
it 'matches when all filters empty' do
|
||||
rule = build(:captain_lifecycle_rule, account: account, filters: {})
|
||||
expect(rule.matches_reservation?(reservation)).to be(true)
|
||||
end
|
||||
|
||||
it 'matches when unit_id in filter' do
|
||||
rule = build(:captain_lifecycle_rule, account: account, filters: { 'unit_ids' => [unit.id] })
|
||||
expect(rule.matches_reservation?(reservation)).to be(true)
|
||||
end
|
||||
|
||||
it 'does not match when unit_id not in filter' do
|
||||
rule = build(:captain_lifecycle_rule, account: account, filters: { 'unit_ids' => [999] })
|
||||
expect(rule.matches_reservation?(reservation)).to be(false)
|
||||
end
|
||||
|
||||
it 'matches when categoria in filter' do
|
||||
rule = build(:captain_lifecycle_rule, account: account, filters: { 'categorias' => ['Alexa'] })
|
||||
expect(rule.matches_reservation?(reservation)).to be(true)
|
||||
end
|
||||
|
||||
it 'does not match when categoria not in filter' do
|
||||
rule = build(:captain_lifecycle_rule, account: account, filters: { 'categorias' => ['Stilo'] })
|
||||
expect(rule.matches_reservation?(reservation)).to be(false)
|
||||
end
|
||||
|
||||
it 'matches when permanencia in filter' do
|
||||
rule = build(:captain_lifecycle_rule, account: account, filters: { 'permanencias' => ['Pernoite'] })
|
||||
expect(rule.matches_reservation?(reservation)).to be(true)
|
||||
end
|
||||
|
||||
it 'requires all specified filters to match' do
|
||||
rule = build(:captain_lifecycle_rule, account: account,
|
||||
filters: { 'unit_ids' => [unit.id], 'categorias' => ['Stilo'] })
|
||||
expect(rule.matches_reservation?(reservation)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
19
spec/factories/captain/reservation.rb
Normal file
19
spec/factories/captain/reservation.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :captain_reservation, class: 'Captain::Reservation' do
|
||||
association :account
|
||||
association :inbox
|
||||
association :contact
|
||||
association :contact_inbox
|
||||
|
||||
suite_identifier { 'Suite 101' }
|
||||
check_in_at { 1.day.from_now.beginning_of_hour }
|
||||
check_out_at { 2.days.from_now.beginning_of_hour }
|
||||
status { :confirmed }
|
||||
total_amount { 300.00 }
|
||||
metadata { {} }
|
||||
|
||||
unit { nil }
|
||||
end
|
||||
end
|
||||
15
spec/factories/captain_lifecycle_rules.rb
Normal file
15
spec/factories/captain_lifecycle_rules.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :captain_lifecycle_rule, class: 'Captain::Lifecycle::Rule' do
|
||||
account
|
||||
sequence(:name) { |n| "Rule ##{n}" }
|
||||
enabled { true }
|
||||
event { 'checkin.scheduled_at' }
|
||||
offset_minutes { -10 }
|
||||
filters { {} }
|
||||
message_type { 'text' }
|
||||
message_body { 'Olá {{ customer.first_name }}, sua suíte {{ reservation.suite }} está pronta!' }
|
||||
priority { 50 }
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user