feat(lifecycle): add Scheduler service and DispatcherJob stub
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4a88f7f517
commit
bb4631f427
13
enterprise/app/jobs/captain/lifecycle/dispatcher_job.rb
Normal file
13
enterprise/app/jobs/captain/lifecycle/dispatcher_job.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Captain::Lifecycle::DispatcherJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def self.perform_at(fire_at, delivery_id)
|
||||
set(wait_until: fire_at).perform_later(delivery_id)
|
||||
end
|
||||
|
||||
def perform(delivery_id)
|
||||
# Stub — full implementation lands in Task 16.
|
||||
end
|
||||
end
|
||||
60
enterprise/app/services/captain/lifecycle/scheduler.rb
Normal file
60
enterprise/app/services/captain/lifecycle/scheduler.rb
Normal file
@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Captain::Lifecycle::Scheduler
|
||||
CHECKIN_EVENTS = %w[checkin.scheduled_at checkin.detected].freeze
|
||||
|
||||
class << self
|
||||
def schedule_for(reservation)
|
||||
rules = Captain::Lifecycle::Rule
|
||||
.where(account_id: reservation.account_id)
|
||||
.active
|
||||
rules.each { |rule| schedule_one_rule(reservation, rule) }
|
||||
end
|
||||
|
||||
def cancel_pending(reservation)
|
||||
Captain::Lifecycle::Delivery
|
||||
.where(captain_reservation_id: reservation.id, status: 'scheduled')
|
||||
.update_all(status: 'cancelled', updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def reschedule_for_checkin_change(reservation)
|
||||
Captain::Lifecycle::Delivery
|
||||
.where(captain_reservation_id: reservation.id, status: 'scheduled')
|
||||
.joins('INNER JOIN captain_lifecycle_rules ON captain_lifecycle_rules.id = captain_lifecycle_deliveries.lifecycle_rule_id')
|
||||
.where(captain_lifecycle_rules: { event: CHECKIN_EVENTS })
|
||||
.update_all(status: 'cancelled', updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
||||
|
||||
Captain::Lifecycle::Rule
|
||||
.where(account_id: reservation.account_id, event: CHECKIN_EVENTS)
|
||||
.active
|
||||
.each { |rule| schedule_one_rule(reservation, rule) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schedule_one_rule(reservation, rule)
|
||||
return unless rule.matches_reservation?(reservation)
|
||||
|
||||
fire_at = compute_fire_at(reservation, rule)
|
||||
return if fire_at.nil? || fire_at <= Time.current
|
||||
|
||||
delivery = Captain::Lifecycle::Delivery.create!(
|
||||
account_id: reservation.account_id,
|
||||
lifecycle_rule_id: rule.id,
|
||||
captain_reservation_id: reservation.id,
|
||||
inbox_id: reservation.unit&.concierge_inbox_id,
|
||||
fire_at: fire_at,
|
||||
status: 'scheduled',
|
||||
origin: 'scheduled_lifecycle'
|
||||
)
|
||||
Captain::Lifecycle::DispatcherJob.perform_at(fire_at, delivery.id)
|
||||
end
|
||||
|
||||
def compute_fire_at(reservation, rule)
|
||||
event_time = Captain::Lifecycle::EventResolver.resolve(reservation, rule.event)
|
||||
return nil if event_time.nil?
|
||||
|
||||
event_time + rule.offset_minutes.minutes
|
||||
end
|
||||
end
|
||||
end
|
||||
103
spec/enterprise/services/captain/lifecycle/scheduler_spec.rb
Normal file
103
spec/enterprise/services/captain/lifecycle/scheduler_spec.rb
Normal file
@ -0,0 +1,103 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Lifecycle::Scheduler do
|
||||
let(:account) { create(:account) }
|
||||
let(:brand) { create(:captain_brand, account: account) }
|
||||
let(:unit) { Captain::Unit.create!(account: account, brand: brand, name: 'Test Unit') }
|
||||
let(:reservation) do
|
||||
create(:captain_reservation,
|
||||
account: account,
|
||||
unit: unit,
|
||||
check_in_at: 2.hours.from_now,
|
||||
check_out_at: 10.hours.from_now)
|
||||
end
|
||||
|
||||
describe '.schedule_for' do
|
||||
let!(:matching_rule) do
|
||||
create(:captain_lifecycle_rule,
|
||||
account: account,
|
||||
event: 'checkin.scheduled_at',
|
||||
offset_minutes: -10)
|
||||
end
|
||||
|
||||
it 'creates a scheduled delivery for each matching enabled rule' do
|
||||
expect { described_class.schedule_for(reservation) }
|
||||
.to change(Captain::Lifecycle::Delivery, :count).by(1)
|
||||
end
|
||||
|
||||
it 'sets fire_at to event_time + offset' do
|
||||
described_class.schedule_for(reservation)
|
||||
delivery = Captain::Lifecycle::Delivery.last
|
||||
expect(delivery.fire_at).to be_within(1.second).of(reservation.check_in_at - 10.minutes)
|
||||
end
|
||||
|
||||
it 'enqueues a Sidekiq job for fire_at' do
|
||||
expect(Captain::Lifecycle::DispatcherJob).to receive(:perform_at)
|
||||
.with(kind_of(Time), kind_of(Integer))
|
||||
described_class.schedule_for(reservation)
|
||||
end
|
||||
|
||||
it 'skips disabled rules' do
|
||||
matching_rule.update!(enabled: false)
|
||||
expect { described_class.schedule_for(reservation) }
|
||||
.not_to change(Captain::Lifecycle::Delivery, :count)
|
||||
end
|
||||
|
||||
it 'skips rules where filter does not match' do
|
||||
matching_rule.update!(filters: { 'categorias' => ['DoesNotExist'] })
|
||||
expect { described_class.schedule_for(reservation) }
|
||||
.not_to change(Captain::Lifecycle::Delivery, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.cancel_pending' do
|
||||
let!(:delivery) do
|
||||
create(:captain_lifecycle_delivery,
|
||||
account: account,
|
||||
captain_reservation: reservation,
|
||||
status: 'scheduled')
|
||||
end
|
||||
let!(:sent) do
|
||||
create(:captain_lifecycle_delivery,
|
||||
account: account,
|
||||
captain_reservation: reservation,
|
||||
status: 'sent')
|
||||
end
|
||||
|
||||
it 'marks scheduled deliveries as cancelled' do
|
||||
described_class.cancel_pending(reservation)
|
||||
expect(delivery.reload.status).to eq('cancelled')
|
||||
end
|
||||
|
||||
it 'does not touch sent deliveries' do
|
||||
described_class.cancel_pending(reservation)
|
||||
expect(sent.reload.status).to eq('sent')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.reschedule_for_checkin_change' do
|
||||
let!(:checkin_rule) do
|
||||
create(:captain_lifecycle_rule,
|
||||
account: account,
|
||||
event: 'checkin.scheduled_at',
|
||||
offset_minutes: -10)
|
||||
end
|
||||
|
||||
before do
|
||||
create(:captain_lifecycle_rule,
|
||||
account: account,
|
||||
event: 'reservation.confirmed',
|
||||
offset_minutes: 0)
|
||||
described_class.schedule_for(reservation)
|
||||
end
|
||||
|
||||
it 'cancels checkin-based scheduled deliveries' do
|
||||
expect { described_class.reschedule_for_checkin_change(reservation) }
|
||||
.to(change do
|
||||
Captain::Lifecycle::Delivery.where(lifecycle_rule_id: checkin_rule.id, status: 'cancelled').count
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user