test(lifecycle): add end-to-end integration spec for scheduler→dispatch→send flow
Also fixes double-scheduling bug in scheduler_spec and delivery_spec caused by after_create_commit hook firing while rules already exist — reservation is now created before rules in setup so the hook finds nothing to schedule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7b009cf47f
commit
f302726d9b
147
spec/enterprise/integration/captain/lifecycle_flow_spec.rb
Normal file
147
spec/enterprise/integration/captain/lifecycle_flow_spec.rb
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user