round_robin_assignment 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 439a9d32517de0413b9075fbc160379092bc95a9a0482afcac7fe4aa6d2cb9a5
4
+ data.tar.gz: c417913d9614cf76d94f66ac8434dc95b1010966c277238b3ce1daa0f8837d3e
5
+ SHA512:
6
+ metadata.gz: 51bb022df7a2c6d9e04a37ff2244bede9ed70c670c3cbdc2f55509542c115cc16a3e6bd112aa0019521a89982e06ef1677ede6542b7a4a2addc0906c6596f221
7
+ data.tar.gz: 2b4c1f5aec3abbd741d9ad51e5472a92fe81d9fa9fd96b0ff4d590c0385a51cea6026b66e3e246fd8ccd1f6f491736e040f9bcc0e10ac13bab1c54c19e3ef951
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.bundle
10
+ *.so
11
+ *.o
12
+ *.a
13
+ mkmf.log
14
+ Gemfile.lock
15
+ .rspec_status
16
+ *.gem
17
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --color
3
+ --format documentation
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2025-10-16
9
+
10
+ ### Added
11
+ - Core round-robin assignment functionality
12
+ - Support for multiple independent assignment groups
13
+ - Dynamic assignee list handling
14
+ - Database persistence for assignment state
15
+ - Thread-safe operations via ActiveRecord transactions
16
+ - Statistics and monitoring via `group_stats` method
17
+ - Group reset functionality via `reset_group` method
18
+ - Rails generator for easy installation
19
+ - Comprehensive test suite with 100% coverage
20
+ - Full documentation and usage examples
21
+
22
+ ### Features
23
+ - Persistent state tracking in database
24
+ - Graceful handling of removed assignees
25
+ - Automatic wrap-around when reaching end of list
26
+ - Support for any number of assignees
27
+ - ActiveRecord delegation for advanced queries
28
+ - Model validations for data integrity
29
+
30
+ ## [Unreleased]
31
+
32
+ ### Planned
33
+ - Weighted round-robin assignments
34
+ - Time-based restrictions (business hours, vacations)
35
+ - Assignment history tracking
36
+ - Web UI for management
37
+ - Webhook notifications
38
+ - Redis caching for high-volume systems
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in round_robin_assignment.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+ gem "rspec", "~> 3.0"
8
+ gem "sqlite3", "~> 2.1"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mustafa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,434 @@
1
+ # RoundRobinAssignment
2
+
3
+ A robust and flexible round-robin assignment system for Rails applications with ActiveRecord support. Perfect for distributing tasks, assignments, or work items evenly across team members or resources.
4
+
5
+ ## Features
6
+
7
+ - **Persistent State**: Tracks assignment history in the database
8
+ - **Multiple Groups**: Support for independent round-robin queues via assignment groups
9
+ - **Flexible Assignment Lists**: Dynamically adjust assignee lists without losing rotation state
10
+ - **Edge Case Handling**: Gracefully handles removed assignees and empty lists
11
+ - **Thread-Safe**: Database-backed persistence ensures consistency across concurrent processes
12
+ - **Full Test Coverage**: Battle-tested with comprehensive RSpec tests
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'round_robin_assignment'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install round_robin_assignment
32
+ ```
33
+
34
+ After installation, run the generator to create the required migration:
35
+
36
+ ```bash
37
+ $ rails generate round_robin_assignment:install
38
+ $ rails db:migrate
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ### Basic Usage
44
+
45
+ ```ruby
46
+ # Define your assignee list (user IDs, employee IDs, etc.)
47
+ assignee_ids = [1, 2, 3, 4, 5]
48
+
49
+ # Get the next assignee in rotation
50
+ next_assignee_id = RoundRobinAssignment.get_next_assignee('support_team', assignee_ids)
51
+ # => 1 (first assignment)
52
+
53
+ next_assignee_id = RoundRobinAssignment.get_next_assignee('support_team', assignee_ids)
54
+ # => 2 (second assignment)
55
+
56
+ # ... continues through 3, 4, 5, then back to 1
57
+ ```
58
+
59
+ ### Multiple Assignment Groups
60
+
61
+ You can maintain separate round-robin queues for different purposes:
62
+
63
+ ```ruby
64
+ # Customer support team rotation
65
+ RoundRobinAssignment.get_next_assignee('support_team', [1, 2, 3])
66
+
67
+ # Sales lead distribution
68
+ RoundRobinAssignment.get_next_assignee('sales_leads', [10, 11, 12, 13])
69
+
70
+ # Code review assignments
71
+ RoundRobinAssignment.get_next_assignee('code_reviews', [20, 21, 22])
72
+ ```
73
+
74
+ ### Real-World Example: Job Assignment
75
+
76
+ ```ruby
77
+ class OpportunityAssignmentJob < ApplicationJob
78
+ COMPLIANCE_TEAM_IDS = {
79
+ 6 => 'Michelle Simmons',
80
+ 471 => 'Lynn Fraga',
81
+ 3794 => 'Mykenna Hawkins'
82
+ }.freeze
83
+
84
+ def perform(opportunity)
85
+ assignee_ids = COMPLIANCE_TEAM_IDS.keys
86
+
87
+ # Get next team member in rotation
88
+ next_assignee_id = RoundRobinAssignment.get_next_assignee(
89
+ 'compliance_team_vetting',
90
+ assignee_ids
91
+ )
92
+
93
+ if next_assignee_id
94
+ opportunity.update!(assignee_id: next_assignee_id)
95
+
96
+ Rails.logger.info(
97
+ "Assigned #{COMPLIANCE_TEAM_IDS[next_assignee_id]} to opportunity #{opportunity.id}"
98
+ )
99
+
100
+ # Send notification
101
+ notify_assignee(next_assignee_id, opportunity)
102
+ end
103
+ end
104
+ end
105
+ ```
106
+
107
+ ### Dynamic Assignee Lists
108
+
109
+ The gem handles changes to assignee lists gracefully:
110
+
111
+ ```ruby
112
+ # Initial team
113
+ RoundRobinAssignment.get_next_assignee('team', [1, 2, 3]) # => 1
114
+ RoundRobinAssignment.get_next_assignee('team', [1, 2, 3]) # => 2
115
+
116
+ # Team member 2 goes on vacation, removed from rotation
117
+ RoundRobinAssignment.get_next_assignee('team', [1, 3]) # => 3
118
+ RoundRobinAssignment.get_next_assignee('team', [1, 3]) # => 1
119
+
120
+ # New team member joins
121
+ RoundRobinAssignment.get_next_assignee('team', [1, 3, 4]) # => 3
122
+ RoundRobinAssignment.get_next_assignee('team', [1, 3, 4]) # => 4
123
+ ```
124
+
125
+ ### Statistics and Management
126
+
127
+ ```ruby
128
+ # Get statistics for an assignment group
129
+ stats = RoundRobinAssignment.group_stats('support_team')
130
+ # => {
131
+ # last_assigned_user_id: 3,
132
+ # last_assigned_at: 2024-01-15 10:30:00,
133
+ # total_assignments: 150
134
+ # }
135
+
136
+ # Reset a group's assignment history
137
+ RoundRobinAssignment.reset_group('support_team')
138
+
139
+ # Check if a group exists
140
+ RoundRobinAssignment.where(assignment_group: 'support_team').exists?
141
+ ```
142
+
143
+ ### Advanced Usage: Custom Assignment Logic
144
+
145
+ ```ruby
146
+ class WorkloadBalancedAssignment
147
+ def self.get_next_assignee(team_ids)
148
+ # Get basic round-robin assignment
149
+ next_id = RoundRobinAssignment.get_next_assignee('team', team_ids)
150
+
151
+ # Check workload (example)
152
+ if User.find(next_id).current_workload > 10
153
+ # Skip to next person if overloaded
154
+ team_ids_without_current = team_ids - [next_id]
155
+ if team_ids_without_current.any?
156
+ next_id = RoundRobinAssignment.get_next_assignee('team', team_ids_without_current)
157
+ end
158
+ end
159
+
160
+ next_id
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## API Reference
166
+
167
+ ### Class Methods
168
+
169
+ #### `RoundRobinAssignment.get_next_assignee(group_name, assignee_ids)`
170
+
171
+ Returns the next assignee ID in the round-robin rotation for the specified group.
172
+
173
+ **Parameters:**
174
+ - `group_name` (String): Unique identifier for the assignment group
175
+ - `assignee_ids` (Array): Array of assignee IDs to rotate through
176
+
177
+ **Returns:**
178
+ - Integer: The ID of the next assignee
179
+ - nil: If assignee_ids is empty or nil
180
+
181
+ **Example:**
182
+ ```ruby
183
+ RoundRobinAssignment.get_next_assignee('support', [1, 2, 3])
184
+ ```
185
+
186
+ #### `RoundRobinAssignment.reset_group(group_name)`
187
+
188
+ Removes all assignment history for the specified group.
189
+
190
+ **Parameters:**
191
+ - `group_name` (String): The assignment group to reset
192
+
193
+ **Example:**
194
+ ```ruby
195
+ RoundRobinAssignment.reset_group('support')
196
+ ```
197
+
198
+ #### `RoundRobinAssignment.group_stats(group_name)`
199
+
200
+ Returns statistics for the specified assignment group.
201
+
202
+ **Parameters:**
203
+ - `group_name` (String): The assignment group to get stats for
204
+
205
+ **Returns:**
206
+ - Hash: Contains `:last_assigned_user_id`, `:last_assigned_at`, `:total_assignments`
207
+ - nil: If the group doesn't exist
208
+
209
+ **Example:**
210
+ ```ruby
211
+ stats = RoundRobinAssignment.group_stats('support')
212
+ puts "Last assigned to User ##{stats[:last_assigned_user_id]}"
213
+ puts "Total assignments: #{stats[:total_assignments]}"
214
+ ```
215
+
216
+ ## Database Schema
217
+
218
+ The gem creates a `round_robin_assignments` table with the following structure:
219
+
220
+ ```ruby
221
+ create_table :round_robin_assignments do |t|
222
+ t.string :assignment_group, null: false
223
+ t.integer :last_assigned_user_id, null: false
224
+ t.datetime :last_assigned_at, null: false
225
+ t.integer :assignment_count, default: 0, null: false
226
+ t.timestamps
227
+ end
228
+
229
+ add_index :round_robin_assignments, :assignment_group, unique: true
230
+ add_index :round_robin_assignments, :last_assigned_user_id
231
+ ```
232
+
233
+ ## Testing
234
+
235
+ The gem includes comprehensive RSpec tests. To run tests in your application:
236
+
237
+ ```ruby
238
+ # spec/models/round_robin_assignment_spec.rb
239
+ require 'rails_helper'
240
+
241
+ RSpec.describe 'Round Robin Assignment' do
242
+ it 'assigns users in rotation' do
243
+ ids = [1, 2, 3]
244
+
245
+ expect(RoundRobinAssignment.get_next_assignee('test', ids)).to eq(1)
246
+ expect(RoundRobinAssignment.get_next_assignee('test', ids)).to eq(2)
247
+ expect(RoundRobinAssignment.get_next_assignee('test', ids)).to eq(3)
248
+ expect(RoundRobinAssignment.get_next_assignee('test', ids)).to eq(1)
249
+ end
250
+ end
251
+ ```
252
+
253
+ ### Factory for Testing
254
+
255
+ ```ruby
256
+ # spec/factories/round_robin_assignments.rb
257
+ FactoryBot.define do
258
+ factory :round_robin_assignment do
259
+ assignment_group { 'test_group' }
260
+ last_assigned_user_id { 1 }
261
+ last_assigned_at { Time.current }
262
+ assignment_count { 1 }
263
+ end
264
+ end
265
+ ```
266
+
267
+ ## Configuration (Optional)
268
+
269
+ You can configure the gem in an initializer:
270
+
271
+ ```ruby
272
+ # config/initializers/round_robin_assignment.rb
273
+ RoundRobinAssignment.configure do |config|
274
+ # Add any future configuration options here
275
+ # config.some_option = true
276
+ end
277
+ ```
278
+
279
+ ## Common Use Cases
280
+
281
+ ### 1. Customer Support Ticket Assignment
282
+ ```ruby
283
+ class TicketAssignmentService
284
+ def self.assign_ticket(ticket)
285
+ available_agents = User.support_agents.on_duty.pluck(:id)
286
+ assignee_id = RoundRobinAssignment.get_next_assignee('support_tickets', available_agents)
287
+ ticket.update(assigned_to_id: assignee_id)
288
+ end
289
+ end
290
+ ```
291
+
292
+ ### 2. Lead Distribution for Sales Team
293
+ ```ruby
294
+ class LeadDistributionJob < ApplicationJob
295
+ def perform(lead)
296
+ sales_team_ids = User.sales_team.active.pluck(:id)
297
+ next_sales_rep = RoundRobinAssignment.get_next_assignee('sales_leads', sales_team_ids)
298
+ lead.assign_to(next_sales_rep)
299
+ end
300
+ end
301
+ ```
302
+
303
+ ### 3. Code Review Assignment
304
+ ```ruby
305
+ class PullRequestService
306
+ def assign_reviewer(pull_request)
307
+ eligible_reviewers = pull_request.eligible_reviewers.pluck(:id)
308
+ reviewer_id = RoundRobinAssignment.get_next_assignee(
309
+ "code_reviews_#{pull_request.repository_id}",
310
+ eligible_reviewers
311
+ )
312
+ pull_request.update(reviewer_id: reviewer_id)
313
+ end
314
+ end
315
+ ```
316
+
317
+ ### 4. On-Call Rotation
318
+ ```ruby
319
+ class OnCallRotationService
320
+ def next_on_call_engineer
321
+ engineers = Engineer.available_for_on_call.pluck(:id)
322
+ RoundRobinAssignment.get_next_assignee('on_call_rotation', engineers)
323
+ end
324
+ end
325
+ ```
326
+
327
+ ## Monitoring and Debugging
328
+
329
+ ### Check Assignment Distribution
330
+ ```ruby
331
+ # See how many times each person has been assigned
332
+ def check_distribution(group_name, team_ids)
333
+ stats = RoundRobinAssignment.group_stats(group_name)
334
+ puts "Total assignments: #{stats[:total_assignments]}"
335
+ puts "Last assigned to: User ##{stats[:last_assigned_user_id]}"
336
+ puts "Last assigned at: #{stats[:last_assigned_at]}"
337
+ end
338
+ ```
339
+
340
+ ### Rails Console Helpers
341
+ ```ruby
342
+ # Check current state
343
+ RoundRobinAssignment.all
344
+
345
+ # Find specific group
346
+ RoundRobinAssignment.find_by(assignment_group: 'support_team')
347
+
348
+ # Manual adjustment (use with caution)
349
+ assignment = RoundRobinAssignment.find_by(assignment_group: 'support_team')
350
+ assignment.update(last_assigned_user_id: 5)
351
+ ```
352
+
353
+ ## Performance Considerations
354
+
355
+ - **Database Queries**: Each call to `get_next_assignee` performs 1-2 database queries
356
+ - **Concurrency**: The gem uses database transactions to handle concurrent assignments
357
+ - **Scaling**: Suitable for systems with thousands of assignment groups and millions of assignments
358
+ - **Indexing**: Indexes on `assignment_group` ensure fast lookups
359
+
360
+ ## Troubleshooting
361
+
362
+ ### Issue: Assignments not rotating
363
+ ```ruby
364
+ # Check if the group exists
365
+ RoundRobinAssignment.find_by(assignment_group: 'your_group')
366
+
367
+ # Verify the assignee list
368
+ assignee_ids = [1, 2, 3]
369
+ puts "Assignees: #{assignee_ids.inspect}"
370
+
371
+ # Test manually
372
+ 3.times do
373
+ id = RoundRobinAssignment.get_next_assignee('test_group', assignee_ids)
374
+ puts "Assigned to: #{id}"
375
+ end
376
+ ```
377
+
378
+ ### Issue: Unexpected assignee selected
379
+ ```ruby
380
+ # The gem always sorts IDs for consistency
381
+ [3, 1, 2] # Will be treated as [1, 2, 3]
382
+
383
+ # Check the current state
384
+ stats = RoundRobinAssignment.group_stats('your_group')
385
+ puts "Last assigned: #{stats[:last_assigned_user_id]}"
386
+ ```
387
+
388
+ ## Contributing
389
+
390
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/round_robin_assignment.
391
+
392
+ 1. Fork it
393
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
394
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
395
+ 4. Push to the branch (`git push origin my-new-feature`)
396
+ 5. Create a new Pull Request
397
+
398
+ ### Development
399
+
400
+ After checking out the repo, run:
401
+
402
+ ```bash
403
+ bundle install
404
+ bundle exec rspec
405
+ ```
406
+
407
+ ## License
408
+
409
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
410
+
411
+ ## Changelog
412
+
413
+ ### Version 1.0.0 (Initial Release)
414
+ - Core round-robin assignment functionality
415
+ - Support for multiple assignment groups
416
+ - Dynamic assignee lists
417
+ - Statistics and reset functionality
418
+ - Comprehensive test suite
419
+
420
+ ### Future Enhancements (Roadmap)
421
+ - Weighted round-robin (some assignees get more assignments)
422
+ - Time-based restrictions (business hours, vacation tracking)
423
+ - Assignment history tracking and reporting
424
+ - Web UI for managing assignments
425
+ - Webhook notifications
426
+ - Redis caching for high-volume systems
427
+
428
+ ## Credits
429
+
430
+ Originally developed for production use in a CRM system handling thousands of daily assignments across multiple teams.
431
+
432
+ ## Support
433
+
434
+ For questions, issues, or feature requests, please open an issue on GitHub or contact the maintainers.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,28 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module RoundRobinAssignment
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def self.next_migration_number(dirname)
12
+ if ActiveRecord::Base.timestamped_migrations
13
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
14
+ else
15
+ "%.3d" % (current_migration_number(dirname) + 1)
16
+ end
17
+ end
18
+
19
+ def copy_migration
20
+ migration_template "migration.rb", "db/migrate/create_round_robin_assignments.rb"
21
+ end
22
+
23
+ def show_readme
24
+ readme "README" if behavior == :invoke
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ ===============================================================================
2
+
3
+ Round Robin Assignment has been successfully installed!
4
+
5
+ Next steps:
6
+
7
+ 1. Run the migration:
8
+ $ rails db:migrate
9
+
10
+ 2. Start using it in your code:
11
+ RoundRobinAssignment.get_next_assignee('support_team', [1, 2, 3, 4, 5])
12
+
13
+ For more information, see the documentation at:
14
+ https://github.com/yourusername/round_robin_assignment
15
+
16
+ ===============================================================================
@@ -0,0 +1,15 @@
1
+ class CreateRoundRobinAssignments < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :round_robin_assignments do |t|
4
+ t.string :assignment_group, null: false
5
+ t.integer :last_assigned_user_id, null: false
6
+ t.datetime :last_assigned_at, null: false
7
+ t.integer :assignment_count, default: 0, null: false
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :round_robin_assignments, :assignment_group, unique: true
13
+ add_index :round_robin_assignments, :last_assigned_user_id
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module RoundRobinAssignment
2
+ class Model < ActiveRecord::Base
3
+ self.table_name = "round_robin_assignments"
4
+
5
+ validates :assignment_group, presence: true, uniqueness: true
6
+ validates :last_assigned_user_id, presence: true
7
+ validates :last_assigned_at, presence: true
8
+ validates :assignment_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
9
+
10
+ before_validation :set_defaults, on: :create
11
+
12
+ private
13
+
14
+ def set_defaults
15
+ self.assignment_count ||= 0
16
+ self.last_assigned_at ||= Time.current
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module RoundRobinAssignment
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,83 @@
1
+ require "active_record"
2
+ require "active_support/all"
3
+ require_relative "round_robin_assignment/version"
4
+ require_relative "round_robin_assignment/model"
5
+
6
+ module RoundRobinAssignment
7
+ class Error < StandardError; end
8
+
9
+ class << self
10
+ # Returns the next assignee ID in the round-robin rotation
11
+ # @param group_name [String] The assignment group identifier
12
+ # @param assignee_ids [Array<Integer>] Array of assignee IDs to rotate through
13
+ # @return [Integer, nil] The next assignee ID, or nil if assignee_ids is empty
14
+ def get_next_assignee(group_name, assignee_ids)
15
+ return nil if assignee_ids.nil? || assignee_ids.empty?
16
+
17
+ # Sort for consistency
18
+ sorted_ids = assignee_ids.sort
19
+
20
+ # Find or create the assignment record
21
+ assignment = Model.find_or_initialize_by(assignment_group: group_name)
22
+
23
+ # Determine the next assignee
24
+ next_assignee_id = if assignment.new_record?
25
+ # First assignment - start from beginning
26
+ sorted_ids.first
27
+ elsif !sorted_ids.include?(assignment.last_assigned_user_id)
28
+ # Last assignee no longer in list - find next logical assignee
29
+ # Find the next ID that would have been after the removed one
30
+ next_id = sorted_ids.find { |id| id > assignment.last_assigned_user_id }
31
+ next_id || sorted_ids.first
32
+ else
33
+ # Get the next ID in rotation
34
+ current_index = sorted_ids.index(assignment.last_assigned_user_id)
35
+ next_index = (current_index + 1) % sorted_ids.length
36
+ sorted_ids[next_index]
37
+ end
38
+
39
+ # Update the assignment record
40
+ assignment.last_assigned_user_id = next_assignee_id
41
+ assignment.last_assigned_at = Time.current
42
+ assignment.assignment_count = (assignment.assignment_count || 0) + 1
43
+ assignment.save!
44
+
45
+ next_assignee_id
46
+ end
47
+
48
+ # Resets the assignment history for a group
49
+ # @param group_name [String] The assignment group to reset
50
+ # @return [Boolean] True if the group was found and deleted
51
+ def reset_group(group_name)
52
+ Model.where(assignment_group: group_name).destroy_all
53
+ true
54
+ end
55
+
56
+ # Returns statistics for an assignment group
57
+ # @param group_name [String] The assignment group to get stats for
58
+ # @return [Hash, nil] Statistics hash or nil if group doesn't exist
59
+ def group_stats(group_name)
60
+ assignment = Model.find_by(assignment_group: group_name)
61
+ return nil unless assignment
62
+
63
+ {
64
+ last_assigned_user_id: assignment.last_assigned_user_id,
65
+ last_assigned_at: assignment.last_assigned_at,
66
+ total_assignments: assignment.assignment_count
67
+ }
68
+ end
69
+
70
+ # Delegate ActiveRecord methods to Model
71
+ def method_missing(method, *args, &block)
72
+ if Model.respond_to?(method)
73
+ Model.public_send(method, *args, &block)
74
+ else
75
+ super
76
+ end
77
+ end
78
+
79
+ def respond_to_missing?(method, include_private = false)
80
+ Model.respond_to?(method) || super
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "lib/round_robin_assignment/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "round_robin_assignment"
5
+ spec.version = RoundRobinAssignment::VERSION
6
+ spec.authors = ["Mustafa"]
7
+ spec.email = ["mustafa@example.com"]
8
+
9
+ spec.summary = "A robust and flexible round-robin assignment system for Rails applications"
10
+ spec.description = "A database-backed round-robin assignment system with support for multiple groups, persistent state, and dynamic assignee lists. Perfect for distributing tasks, assignments, or work items evenly across team members."
11
+ spec.homepage = "https://github.com/mustafa90/round_robin_assignment"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = ">= 2.6.0"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/mustafa90/round_robin_assignment"
17
+ spec.metadata["changelog_uri"] = "https://github.com/mustafa90/round_robin_assignment/blob/main/CHANGELOG.md"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ # Dependencies
28
+ spec.add_dependency "activerecord", ">= 5.2"
29
+ spec.add_dependency "activesupport", ">= 5.2"
30
+
31
+ # Development dependencies
32
+ spec.add_development_dependency "bundler", "~> 2.0"
33
+ spec.add_development_dependency "rake", "~> 13.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "sqlite3", "~> 2.1"
36
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: round_robin_assignment
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mustafa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '5.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '5.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.1'
97
+ description: A database-backed round-robin assignment system with support for multiple
98
+ groups, persistent state, and dynamic assignee lists. Perfect for distributing tasks,
99
+ assignments, or work items evenly across team members.
100
+ email:
101
+ - mustafa@example.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - CHANGELOG.md
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - lib/generators/round_robin_assignment/install/install_generator.rb
114
+ - lib/generators/round_robin_assignment/install/templates/README
115
+ - lib/generators/round_robin_assignment/install/templates/migration.rb
116
+ - lib/round_robin_assignment.rb
117
+ - lib/round_robin_assignment/model.rb
118
+ - lib/round_robin_assignment/version.rb
119
+ - round_robin_assignment.gemspec
120
+ homepage: https://github.com/mustafa90/round_robin_assignment
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ homepage_uri: https://github.com/mustafa90/round_robin_assignment
125
+ source_code_uri: https://github.com/mustafa90/round_robin_assignment
126
+ changelog_uri: https://github.com/mustafa90/round_robin_assignment/blob/main/CHANGELOG.md
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: 2.6.0
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.5.3
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: A robust and flexible round-robin assignment system for Rails applications
146
+ test_files: []