Module: ConcernsOnRails::Models::Sequenceable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/models/sequenceable.rb

Overview

Generates ordered, human-friendly sequential reference numbers — invoice numbers, order numbers, ticket numbers, support cases. Unlike Hashable / Tokenizable (which produce random identifiers), Sequenceable produces ordered ones backed by an integer column that is the source of truth.

class Invoice < ApplicationRecord
  include ConcernsOnRails::Sequenceable

  sequenceable_by :sequence,        # integer column — source of truth
    into:    :number,               # optional string column for the formatted value
    prefix:  "INV-",
    padding: 5,
    scope:   :account_id,           # one counter per account
    reset:   :year                  # restart numbering each calendar year
end

invoice = Invoice.create!(account_id: 1)
invoice.sequence            # => 1, 2, 3 ... (per account, per year)
invoice.number              # => "INV-2026-00001"
invoice.formatted_sequence  # => "INV-2026-00001"
Invoice.next_sequence(account_id: 1)  # peek the next value without creating

The integer is computed as MAX(field) within the scope (+ period) + 1, so numbering is dense and ordered. Generation is best-effort under concurrency — pair the column(s) with a scoped unique DB index for a real guarantee.

Constant Summary collapse

RESET_PERIODS =
%i[never year month day].freeze
MAX_GENERATION_ATTEMPTS =
10
NAME =
"ConcernsOnRails::Models::Sequenceable".freeze

Instance Method Summary collapse

Instance Method Details

#assign_sequenceable_value(field) ⇒ Object

Assigns the sequence (and, when configured, the formatted string) only when the integer column is blank, so callers can pass an explicit value. The increment-until-free loop is a best-effort guard against pre-taken values; a scoped unique index is the real concurrency guarantee.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/concerns_on_rails/models/sequenceable.rb', line 112

def assign_sequenceable_value(field)
  cfg = self.class.sequenceable_config.fetch(field)

  if self[field].blank?
    candidate = self.class.send(:sequence_base_value, field, self, {})
    attempts = 0
    while self.class.send(:sequence_value_taken?, field, candidate, self, {})
      attempts += 1
      if attempts >= MAX_GENERATION_ATTEMPTS
        raise "#{NAME}: could not find a free value for '#{field}' after " \
              "#{MAX_GENERATION_ATTEMPTS} attempts — add a scoped unique index"
      end
      candidate += 1
    end
    self[field] = candidate
  end

  return unless cfg[:into] && self[cfg[:into]].blank?

  self[cfg[:into]] = self.class.send(:format_sequence, field, self[field], self)
end