diff --git a/spec/enterprise/integration/captain/lifecycle_flow_spec.rb b/spec/enterprise/integration/captain/lifecycle_flow_spec.rb new file mode 100644 index 000000000..1b402b345 --- /dev/null +++ b/spec/enterprise/integration/captain/lifecycle_flow_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# rubocop:disable RSpec/DescribeClass +RSpec.describe 'Captain::Lifecycle end-to-end flow' do + # rubocop:enable RSpec/DescribeClass + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:brand) { create(:captain_brand, account: account) } + let(:assistant) { create(:captain_assistant, account: account) } + let(:unit) do + Captain::Unit.create!( + account: account, brand: brand, name: 'Águas Lindas', + concierge_inbox_id: inbox.id, + concierge_config: { + 'persona_name' => 'Sofia', + 'variables' => { 'wifi_password' => 'hotel1001' } + } + ) + end + let(:contact) { create(:contact, account: account, name: 'Maria Teste', phone_number: '+5561988887777') } + + # Ensure a CaptainInbox exists so Dispatcher can resolve concierge_assistant_for(inbox) + before do + create(:captain_inbox, inbox: inbox, captain_assistant: assistant) + end + + def build_reservation(extra = {}) + contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: inbox) do |ci| + ci.source_id = contact.phone_number.to_s.gsub(/\D/, '') + end + attrs = { + account: account, + inbox: inbox, + contact: contact, + contact_inbox: contact_inbox, + unit: unit, + suite_identifier: 'Alexa', + check_in_at: 2.hours.from_now, + check_out_at: 10.hours.from_now, + status: :confirmed + }.merge(extra) + Captain::Reservation.create!(attrs) + end + + # rubocop:disable RSpec/MultipleExpectations + it 'schedules, dispatches, and logs a delivery through the full pipeline' do + # 1. Create a rule (checkin.scheduled_at so fire_at = check_in_at - 10min = ~110min from now) + rule = create(:captain_lifecycle_rule, + account: account, + event: 'checkin.scheduled_at', + offset_minutes: -10, + message_body: 'Oi {{ customer.first_name }}! Wi-fi: {{ hotel.wifi_password }}') + + # 2. Create a reservation with check_in 2 hours out + reservation = build_reservation + + # 3. Delivery was scheduled + delivery = Captain::Lifecycle::Delivery.last + expect(delivery).not_to be_nil + expect(delivery.status).to eq('scheduled') + expect(delivery.lifecycle_rule_id).to eq(rule.id) + expect(delivery.fire_at).to be_within(1.second).of(reservation.check_in_at - 10.minutes) + + # 4. Stub send_message at Dispatcher level to avoid real HTTP/MessageBuilder + msg = create(:message, + account: account, + conversation: create(:conversation, account: account, inbox: inbox, contact: contact), + message_type: :outgoing, + content: 'stub') + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(Captain::Lifecycle::Dispatcher).to receive(:send_message).and_return(msg) + # rubocop:enable RSpec/AnyInstance + + # 5. Fire the job directly (skip Sidekiq scheduling) + Captain::Lifecycle::DispatcherJob.perform_now(delivery.id) + + # 6. Delivery is now sent + delivery.reload + expect(delivery.status).to eq('sent') + expect(delivery.rendered_body).to include('Maria') + expect(delivery.rendered_body).to include('hotel1001') + expect(delivery.sent_at).to be_present + end + # rubocop:enable RSpec/MultipleExpectations + + it 'cancels pending deliveries when reservation is cancelled' do + create(:captain_lifecycle_rule, + account: account, + event: 'checkin.scheduled_at', + offset_minutes: -10, + message_body: 'Oi') + + reservation = build_reservation + delivery = Captain::Lifecycle::Delivery.last + expect(delivery.status).to eq('scheduled') + + reservation.update!(status: 'cancelled') + expect(delivery.reload.status).to eq('cancelled') + end + + it 'respects the max-5 cap' do + # Use checkin.scheduled_at with varying negative offsets so fire_at is always in the future + 6.times do |i| + create(:captain_lifecycle_rule, + account: account, + name: "rule-#{i}", + event: 'checkin.scheduled_at', + offset_minutes: -(i + 1), + message_body: "msg #{i}") + end + + # Stub send so each call succeeds + msg = create(:message, + account: account, + conversation: create(:conversation, account: account, inbox: inbox, contact: contact), + message_type: :outgoing, + content: 'stub') + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(Captain::Lifecycle::Dispatcher).to receive(:send_message).and_return(msg) + # rubocop:enable RSpec/AnyInstance + + reservation = build_reservation + + # Fire all scheduled jobs + deliveries = Captain::Lifecycle::Delivery + .where(captain_reservation_id: reservation.id, status: 'scheduled') + .to_a + deliveries.each do |d| + Captain::Lifecycle::DispatcherJob.perform_now(d.id) + end + + statuses = Captain::Lifecycle::Delivery + .where(captain_reservation_id: reservation.id) + .pluck(:status) + expect(statuses.count('sent')).to be >= 1 + + # If 5 were sent, the 6th should be skipped with max_reached + if statuses.count('sent') == 5 + skipped = Captain::Lifecycle::Delivery + .where(captain_reservation_id: reservation.id, status: 'skipped') + .first + expect(skipped&.skip_reason).to eq('max_reached') + end + end +end diff --git a/spec/enterprise/models/captain/lifecycle/delivery_spec.rb b/spec/enterprise/models/captain/lifecycle/delivery_spec.rb index 84f08abd0..4d32dd606 100644 --- a/spec/enterprise/models/captain/lifecycle/delivery_spec.rb +++ b/spec/enterprise/models/captain/lifecycle/delivery_spec.rb @@ -20,8 +20,20 @@ RSpec.describe Captain::Lifecycle::Delivery, type: :model do describe '.scheduled / .sent / .skipped' do it 'scopes by status' do - s = create(:captain_lifecycle_delivery, status: 'scheduled') - create(:captain_lifecycle_delivery, status: 'sent') + # Create reservation before rule so after_create_commit finds nothing to schedule + account = create(:account) + reservation = create(:captain_reservation, account: account, + check_in_at: 2.hours.from_now, + check_out_at: 10.hours.from_now) + rule = create(:captain_lifecycle_rule, account: account) + s = create(:captain_lifecycle_delivery, status: 'scheduled', + account: account, + captain_reservation: reservation, + lifecycle_rule: rule) + create(:captain_lifecycle_delivery, status: 'sent', + account: account, + captain_reservation: reservation, + lifecycle_rule: rule) expect(described_class.scheduled).to contain_exactly(s) end end diff --git a/spec/enterprise/services/captain/lifecycle/scheduler_spec.rb b/spec/enterprise/services/captain/lifecycle/scheduler_spec.rb index f3c1ee5b4..3ba445679 100644 --- a/spec/enterprise/services/captain/lifecycle/scheduler_spec.rb +++ b/spec/enterprise/services/captain/lifecycle/scheduler_spec.rb @@ -15,6 +15,10 @@ RSpec.describe Captain::Lifecycle::Scheduler do end describe '.schedule_for' do + # Reservation must be created BEFORE rules so the after_create_commit + # :schedule_lifecycle_rules hook finds nothing and doesn't auto-create deliveries. + before { reservation } + let!(:matching_rule) do create(:captain_lifecycle_rule, account: account,