Module: StandardLedger::Entry

Extended by:
ActiveSupport::Concern
Defined in:
lib/standard_ledger/entry.rb

Overview

Marks an ActiveRecord model as a ledger entry: an immutable, append-only row that may project onto one or more aggregate targets.

Including this concern installs:

- the `ledger_entry` class macro (declares immutability + idempotency)
- read-only behavior post-creation (when `immutable: true`, the default)
- idempotency-by-unique-index (when `idempotency_key:` is non-nil)

Projection registration happens via the separate ‘Projector` concern —the two are decoupled so that an entry can be marked immutable without also opting into projections, and vice versa.

Examples:

class VoucherRecord < ApplicationRecord
  include StandardLedger::Entry

  ledger_entry kind:            :action,
               idempotency_key: :serial_no,
               scope:           :organisation_id
end

Instance Method Summary collapse

Instance Method Details

#idempotent?Boolean

Returns true when this row was returned from an idempotent ‘create!` rescue — i.e. an existing row matched the unique constraint and was returned instead of inserted.

Returns:

  • (Boolean)


186
187
188
# File 'lib/standard_ledger/entry.rb', line 186

def idempotent?
  !!@_standard_ledger_idempotent
end

#readonly?Boolean

AR consults ‘readonly?` from `save`/`update` paths; raising ReadOnlyRecord here matches the ActiveRecord contract for persisted immutable rows. New, unpersisted instances stay writable so the initial INSERT can land.

Returns:

  • (Boolean)


194
195
196
197
198
# File 'lib/standard_ledger/entry.rb', line 194

def readonly?
  return super unless standard_ledger_immutable?

  !new_record?
end

#standard_ledger_targetsHash{Symbol => ActiveRecord::Base}

Returns the entry’s belongs_to targets keyed by association name. Used by the ‘entry.created` notification payload and by `StandardLedger.post`’s telemetry. Skips polymorphic and missing associations so the payload only includes what’s actually present.

Performance trade-off: this fires from ‘after_commit`, where AR may have cleared the association cache. Each `public_send(reflection.name)` can therefore issue a SELECT to reload the cached target. For the typical 1–2 belongs_to entry, that’s negligible. If profiling on a high-cardinality entry shows this matters, capture targets earlier (e.g. in ‘before_create`) and stash them on the instance — deferred to a future PR. Notably, an inline-mode caller has already resolved these targets by the time `after_commit` runs, so the SELECTs would only happen for entries with belongs_to associations that are not registered as projection targets.

Returns:

  • (Hash{Symbol => ActiveRecord::Base})


217
218
219
220
221
222
223
224
225
226
# File 'lib/standard_ledger/entry.rb', line 217

def standard_ledger_targets
  return {} unless self.class.respond_to?(:reflect_on_all_associations)

  self.class.reflect_on_all_associations(:belongs_to).each_with_object({}) do |reflection, memo|
    next if reflection.polymorphic?

    target = public_send(reflection.name)
    memo[reflection.name] = target unless target.nil?
  end
end