Skip to content

Instantly share code, notes, and snippets.

@blocknotes
Last active September 12, 2022 18:55
Show Gist options
  • Save blocknotes/d4f5fde5cd1d49a279f279a45420027d to your computer and use it in GitHub Desktop.
Save blocknotes/d4f5fde5cd1d49a279f279a45420027d to your computer and use it in GitHub Desktop.
Rails simple versioning
# db/migrate/20220909010101_create_price_lists.rb
class CreatePriceLists < ActiveRecord::Migration[7.0]
def change
create_table :price_lists do |t|
t.string :name, null: false
t.integer :version, null: false, default: 1
t.references :source_price_list
t.jsonb :price_items_changes
t.timestamps
end
end
end
# db/migrate/20220909010102_create_price_items.rb
class CreatePriceItems < ActiveRecord::Migration[7.0]
def change
create_table :price_items do |t|
t.string :code, null: false
t.string :name
t.decimal :price
t.boolean :draft, null: false, default: true
t.boolean :deleted, null: false, default: false
t.references :price_list
t.references :source_item
t.timestamps
end
add_index :price_items, %i[price_list_id draft code], unique: true, order: { draft: :desc, code: :asc }
end
end
# app/models/price_item.rb
class PriceItem < ApplicationRecord
belongs_to :price_list
belongs_to :source_item, class_name: 'PriceItem', foreign_key: 'source_item_id', optional: true
validates :code, presence: true, uniqueness: {
scope: %i[price_list_id draft], message: 'must be unique per price list and draft'
}
validates_presence_of :name, unless: :deleted?
validates_presence_of :price, unless: :deleted?
def to_s
"\"#{name}\" [#{code}] #{price}"
end
end
# app/models/price_list.rb
class PriceList < ApplicationRecord
has_many :price_items, -> { where(draft: false) }
has_many :draft_price_items, -> { where(draft: true) }, { class_name: 'PriceItem' }
has_many :all_price_items, -> { order(draft: :desc, code: :asc) }, { class_name: 'PriceItem', dependent: :destroy }
belongs_to :source_price_list, class_name: 'PriceList', foreign_key: 'source_price_list_id', optional: true
validates_presence_of :name
validates_presence_of :version
def create_new_version!(store_changes: true)
PriceList.transaction do
price_list = PriceList.create!(name: name, source_price_list: self, version: version + 1)
logs = _prepare_items(price_list: price_list)
logs += _remove_deleted_items
update_column(:price_items_changes, logs) if store_changes
price_list
end
end
def current_price_items
all_price_items.uniq(&:code).reject(&:deleted?)
end
def dirty?
draft_price_items.any?
end
def update_item!(attrs)
if attrs[:source_item].is_a?(PriceItem)
draft_item_attrs = attrs[:source_item].slice(*%w[code name price]).symbolize_keys.merge!(attrs)
item = draft_price_items.find_by(source_item: attrs[:source_item]) || draft_price_items.build
item.update!(draft_item_attrs.except!(:draft, :price_list_id, :source_item_id))
else
draft_item_attrs = attrs.slice(*%i[code name price deleted source_item])
draft_price_items.create!(draft_item_attrs.except!(:draft, :price_list_id, :source_item_id))
end
end
def to_s
result = "Price list \"#{name}\" - version #{version}:\n"
result << "#{price_items.count} price_items\n"
price_items.order(:code).each { |price_item| result << "- #{price_item}\n" }
result << "#{draft_price_items.count} draft_price_items\n"
draft_price_items.order(:code).each do |price_item|
result << "- #{price_item}#{price_item.deleted? ? ' => DELETE' : ''}\n"
end
result
end
private
def _copy_item(price_list:, price_item:)
attrs = price_item.attributes.slice('name', 'code', 'price')
price_list.price_items.create!(attrs)
nil
end
def _create_or_update_item(price_list:, price_item:)
price_item.update!(price_list: price_list, draft: false)
if price_item.source_item
{ type: :update, code: price_item.code, price: price_item.price, old_price: price_item.source_item.price }
else
{ type: :create, code: price_item.code, price: price_item.price }
end
end
def _prepare_items(price_list:)
current_price_items.map do |price_item|
if price_item.draft?
_create_or_update_item(price_list: price_list, price_item: price_item)
else
_copy_item(price_list: price_list, price_item: price_item)
end
end.compact
end
def _remove_deleted_items
draft_price_items.destroy_all.map do |item|
{ type: :delete, code: item.code }
end
end
end
@blocknotes
Copy link
Author

blocknotes commented Sep 10, 2022

Testing:

PriceList.create!(name: 'list 1')

pl = PriceList.last
itm1 = pl.price_items.create!(code: 'ITM1', name: 'item 1', price: 11)
itm2 = pl.price_items.create!(code: 'ITM2', name: 'item 2', price: 22)
itm3 = pl.price_items.create!(code: 'ITM3', name: 'item 3', price: 33)
pl.update_item!(code: 'ITM4', name: 'item 4', price: 44) # New incoming item
pl.update_item!(source_item: itm3, name: 'item 3.1', price: 31.111) # Update existing item
pl.update_item!(source_item: itm2, deleted: true) # Delete item
pl.update_item!(source_item: itm3, name: 'item 3.2', price: 32.222) # Modify a modified item
puts pl

pl.create_new_version!
puts pl
puts PriceList.last
puts pl.price_items_changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment