Skip to content

Commit 9312a82

Browse files
mroderickopencode
andcommitted
feat(invitations): add InvitationLogger service
- Service for logging invitation batch operations - Tracks success/failure/skip counts per batch - Provides convenience methods for starting/finishing/failing batches Co-Authored-By: opencode <noreply@opencode.ai>
1 parent 5db1552 commit 9312a82

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

app/services/invitation_logger.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
class InvitationLogger
2+
def initialize(loggable, initiator, audience, action)
3+
@loggable = loggable
4+
@initiator = initiator
5+
@audience = audience
6+
@action = action
7+
@log = nil
8+
end
9+
10+
def start_batch
11+
@log = InvitationLog.create!(
12+
loggable: @loggable,
13+
initiator: @initiator,
14+
chapter_id: @loggable.try(:chapter_id),
15+
audience: @audience,
16+
action: @action,
17+
started_at: Time.current,
18+
status: :running
19+
)
20+
end
21+
22+
def log_success(member, invitation = nil)
23+
return unless @log
24+
25+
@log.entries.create!(
26+
member: member,
27+
invitation: invitation,
28+
status: :success,
29+
processed_at: Time.current
30+
).tap { @log.increment!(:success_count) }
31+
end
32+
33+
def log_failure(member, invitation, error)
34+
return unless @log
35+
36+
@log.entries.create!(
37+
member: member,
38+
invitation: invitation,
39+
status: :failed,
40+
failure_reason: error.message,
41+
processed_at: Time.current
42+
).tap { @log.increment!(:failure_count) }
43+
end
44+
45+
def log_skipped(member, invitation, reason)
46+
return unless @log
47+
48+
@log.entries.create!(
49+
member: member,
50+
invitation: invitation,
51+
status: :skipped,
52+
failure_reason: reason,
53+
processed_at: Time.current
54+
).tap { @log.increment!(:skipped_count) }
55+
end
56+
57+
def finish_batch(total_invitees)
58+
return unless @log
59+
60+
@log.update!(
61+
total_invitees: total_invitees,
62+
completed_at: Time.current,
63+
status: :completed
64+
)
65+
end
66+
67+
def fail_batch(error)
68+
return unless @log
69+
70+
@log.update!(
71+
status: :failed,
72+
error_message: error.message,
73+
completed_at: Time.current
74+
)
75+
end
76+
77+
attr_reader :log
78+
end
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe InvitationLogger do
4+
let(:workshop) { Fabricate(:workshop) }
5+
let(:initiator) { Fabricate(:member) }
6+
let(:member) { Fabricate(:member) }
7+
let(:invitation) { Fabricate(:workshop_invitation, workshop: workshop, member: member) }
8+
9+
describe '#start_batch' do
10+
it 'creates an InvitationLog record' do
11+
logger = described_class.new(workshop, initiator, 'students', :invite)
12+
log = logger.start_batch
13+
14+
expect(log).to be_persisted
15+
expect(log.loggable).to eq workshop
16+
expect(log.initiator).to eq initiator
17+
expect(log.audience).to eq 'students'
18+
expect(log.action).to eq 'invite'
19+
expect(log.status).to eq 'running'
20+
end
21+
22+
it 'raises error if a running batch already exists' do
23+
Fabricate(:invitation_log, loggable: workshop, audience: 'students', action: 'invite', status: :running)
24+
logger = described_class.new(workshop, initiator, 'students', :invite)
25+
26+
expect { logger.start_batch }.to raise_error(ActiveRecord::RecordNotUnique)
27+
end
28+
29+
it 'sets chapter_id from loggable' do
30+
logger = described_class.new(workshop, initiator, 'students', :invite)
31+
log = logger.start_batch
32+
33+
expect(log.chapter_id).to eq workshop.chapter_id
34+
end
35+
end
36+
37+
describe '#log_success' do
38+
let(:logger) { described_class.new(workshop, initiator, 'students', :invite) }
39+
let!(:log) { logger.start_batch }
40+
41+
it 'creates entry with success status and increments count' do
42+
entry = logger.log_success(member, invitation)
43+
44+
expect(entry.status).to eq 'success'
45+
expect(entry.member).to eq member
46+
expect(entry.invitation).to eq invitation
47+
expect(log.reload.success_count).to eq 1
48+
end
49+
end
50+
51+
describe '#log_failure' do
52+
let(:logger) { described_class.new(workshop, initiator, 'students', :invite) }
53+
let!(:log) { logger.start_batch }
54+
55+
it 'creates entry with failure status and increments count' do
56+
error = StandardError.new('SMTP error')
57+
entry = logger.log_failure(member, invitation, error)
58+
59+
expect(entry.status).to eq 'failed'
60+
expect(entry.failure_reason).to eq 'SMTP error'
61+
expect(log.reload.failure_count).to eq 1
62+
end
63+
end
64+
65+
describe '#log_skipped' do
66+
let(:logger) { described_class.new(workshop, initiator, 'students', :invite) }
67+
let!(:log) { logger.start_batch }
68+
69+
it 'creates entry with skipped status and increments count' do
70+
entry = logger.log_skipped(member, invitation, 'Already invited')
71+
72+
expect(entry.status).to eq 'skipped'
73+
expect(entry.failure_reason).to eq 'Already invited'
74+
expect(log.reload.skipped_count).to eq 1
75+
end
76+
end
77+
78+
describe '#finish_batch' do
79+
let(:logger) { described_class.new(workshop, initiator, 'students', :invite) }
80+
let!(:log) { logger.start_batch }
81+
82+
it 'updates status to completed with counts' do
83+
2.times { logger.log_success(Fabricate(:member), Fabricate(:workshop_invitation, workshop: workshop)) }
84+
logger.log_failure(Fabricate(:member), Fabricate(:workshop_invitation, workshop: workshop), StandardError.new('err'))
85+
86+
logger.finish_batch(5)
87+
88+
expect(log.reload.status).to eq 'completed'
89+
expect(log.total_invitees).to eq 5
90+
expect(log.completed_at).to be_present
91+
end
92+
end
93+
94+
describe '#fail_batch' do
95+
let(:logger) { described_class.new(workshop, initiator, 'students', :invite) }
96+
let!(:log) { logger.start_batch }
97+
98+
it 'updates status to failed with error message' do
99+
error = StandardError.new('Something went wrong')
100+
logger.fail_batch(error)
101+
102+
expect(log.reload.status).to eq 'failed'
103+
expect(log.error_message).to eq 'Something went wrong'
104+
expect(log.completed_at).to be_present
105+
end
106+
end
107+
end

0 commit comments

Comments
 (0)