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
-
#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.
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 |