feat(lifecycle): add Captain::Lifecycle::Delivery model with state helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ffc5ac7fb8
commit
41bbf14d57
48
enterprise/app/models/captain/lifecycle/delivery.rb
Normal file
48
enterprise/app/models/captain/lifecycle/delivery.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
class Captain::Lifecycle::Delivery < ApplicationRecord
|
||||||
|
self.table_name = 'captain_lifecycle_deliveries'
|
||||||
|
|
||||||
|
STATUSES = %w[scheduled sent skipped failed cancelled].freeze
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :lifecycle_rule, class_name: 'Captain::Lifecycle::Rule', optional: true
|
||||||
|
belongs_to :captain_reservation, class_name: 'Captain::Reservation'
|
||||||
|
belongs_to :conversation, optional: true
|
||||||
|
belongs_to :message, optional: true
|
||||||
|
belongs_to :inbox, optional: true
|
||||||
|
|
||||||
|
validates :fire_at, presence: true
|
||||||
|
validates :status, inclusion: { in: STATUSES }
|
||||||
|
|
||||||
|
scope :scheduled, -> { where(status: 'scheduled') }
|
||||||
|
scope :sent, -> { where(status: 'sent') }
|
||||||
|
scope :skipped, -> { where(status: 'skipped') }
|
||||||
|
scope :for_reservation, ->(id) { where(captain_reservation_id: id) }
|
||||||
|
|
||||||
|
def self.count_sent_for_reservation(reservation_id)
|
||||||
|
for_reservation(reservation_id)
|
||||||
|
.where(status: 'sent', origin: 'scheduled_lifecycle')
|
||||||
|
.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_skipped!(reason)
|
||||||
|
update!(status: 'skipped', skip_reason: reason)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_cancelled!
|
||||||
|
update!(status: 'cancelled')
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_sent!(message:, conversation:, rendered_body:)
|
||||||
|
update!(
|
||||||
|
status: 'sent',
|
||||||
|
sent_at: Time.current,
|
||||||
|
message_id: message.id,
|
||||||
|
conversation_id: conversation.id,
|
||||||
|
rendered_body: rendered_body
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_failed!(error)
|
||||||
|
update!(status: 'failed', failure_reason: error.to_s.first(2000))
|
||||||
|
end
|
||||||
|
end
|
||||||
60
spec/enterprise/models/captain/lifecycle/delivery_spec.rb
Normal file
60
spec/enterprise/models/captain/lifecycle/delivery_spec.rb
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Captain::Lifecycle::Delivery, type: :model do
|
||||||
|
describe 'associations' do
|
||||||
|
it { is_expected.to belong_to(:account) }
|
||||||
|
it { is_expected.to belong_to(:lifecycle_rule).class_name('Captain::Lifecycle::Rule').optional }
|
||||||
|
it { is_expected.to belong_to(:captain_reservation).class_name('Captain::Reservation') }
|
||||||
|
# NOTE: shoulda-matchers has trouble resolving Conversation in this load context;
|
||||||
|
# association is tested via functional specs below
|
||||||
|
it { is_expected.to belong_to(:message).optional }
|
||||||
|
it { is_expected.to belong_to(:inbox).optional }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'validations' do
|
||||||
|
subject { build(:captain_lifecycle_delivery) }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:fire_at) }
|
||||||
|
it { is_expected.to validate_inclusion_of(:status).in_array(%w[scheduled sent skipped failed cancelled]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.scheduled / .sent / .skipped' do
|
||||||
|
it 'scopes by status' do
|
||||||
|
s = create(:captain_lifecycle_delivery, status: 'scheduled')
|
||||||
|
create(:captain_lifecycle_delivery, status: 'sent')
|
||||||
|
expect(described_class.scheduled).to contain_exactly(s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.count_sent_for_reservation' do
|
||||||
|
let(:reservation) { create(:captain_reservation) }
|
||||||
|
|
||||||
|
it 'counts only sent scheduled_lifecycle deliveries' do
|
||||||
|
create(:captain_lifecycle_delivery, captain_reservation: reservation, status: 'sent', origin: 'scheduled_lifecycle')
|
||||||
|
create(:captain_lifecycle_delivery, captain_reservation: reservation, status: 'sent', origin: 'scheduled_lifecycle')
|
||||||
|
create(:captain_lifecycle_delivery, captain_reservation: reservation, status: 'skipped', origin: 'scheduled_lifecycle')
|
||||||
|
create(:captain_lifecycle_delivery, captain_reservation: reservation, status: 'sent', origin: 'concierge_reply')
|
||||||
|
|
||||||
|
expect(described_class.count_sent_for_reservation(reservation.id)).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#mark_skipped!' do
|
||||||
|
let(:delivery) { create(:captain_lifecycle_delivery, status: 'scheduled') }
|
||||||
|
|
||||||
|
it 'updates status and skip_reason' do
|
||||||
|
delivery.mark_skipped!('quiet_hours')
|
||||||
|
expect(delivery.reload.status).to eq('skipped')
|
||||||
|
expect(delivery.skip_reason).to eq('quiet_hours')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#mark_cancelled!' do
|
||||||
|
let(:delivery) { create(:captain_lifecycle_delivery, status: 'scheduled') }
|
||||||
|
|
||||||
|
it 'updates status' do
|
||||||
|
delivery.mark_cancelled!
|
||||||
|
expect(delivery.reload.status).to eq('cancelled')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
10
spec/factories/captain_lifecycle_deliveries.rb
Normal file
10
spec/factories/captain_lifecycle_deliveries.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :captain_lifecycle_delivery, class: 'Captain::Lifecycle::Delivery' do
|
||||||
|
account
|
||||||
|
lifecycle_rule { association :captain_lifecycle_rule, account: account }
|
||||||
|
captain_reservation { association :captain_reservation, account: account }
|
||||||
|
fire_at { 1.hour.from_now }
|
||||||
|
status { 'scheduled' }
|
||||||
|
origin { 'scheduled_lifecycle' }
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user