diff --git a/app/events/adjustment_event.rb b/app/events/adjustment_event.rb index a472e5f744..e0f1978c57 100644 --- a/app/events/adjustment_event.rb +++ b/app/events/adjustment_event.rb @@ -4,7 +4,7 @@ def self.publish(adjustment) create( eventable: adjustment, organization_id: adjustment.organization_id, - event_time: Time.zone.now, + event_time: adjustment.created_at, data: EventTypes::InventoryPayload.new( items: EventTypes::EventLineItem.from_line_items(adjustment.line_items, to: adjustment.storage_location_id) ) diff --git a/app/events/audit_event.rb b/app/events/audit_event.rb index 247400ae94..c4ed421e8e 100644 --- a/app/events/audit_event.rb +++ b/app/events/audit_event.rb @@ -6,7 +6,7 @@ def self.publish(audit) create( eventable: audit, organization_id: audit.organization_id, - event_time: Time.zone.now, + event_time: audit.updated_at, data: EventTypes::AuditPayload.new( storage_location_id: audit.storage_location_id, items: EventTypes::EventLineItem.from_line_items(audit.line_items, to: audit.storage_location_id) diff --git a/app/events/distribution_event.rb b/app/events/distribution_event.rb index 6efc7f6984..11a256172a 100644 --- a/app/events/distribution_event.rb +++ b/app/events/distribution_event.rb @@ -4,7 +4,7 @@ def self.publish(distribution) create( eventable: distribution, organization_id: distribution.organization_id, - event_time: Time.zone.now, + event_time: distribution.created_at, data: EventTypes::InventoryPayload.new( items: EventTypes::EventLineItem.from_line_items(distribution.line_items, from: distribution.storage_location_id) ) diff --git a/app/events/donation_event.rb b/app/events/donation_event.rb index 83f5207d76..adf923c461 100644 --- a/app/events/donation_event.rb +++ b/app/events/donation_event.rb @@ -4,7 +4,7 @@ def self.publish(donation) create( eventable: donation, organization_id: donation.organization_id, - event_time: Time.zone.now, + event_time: donation.created_at, data: EventTypes::InventoryPayload.new( items: EventTypes::EventLineItem.from_line_items(donation.line_items, to: donation.storage_location_id) ) diff --git a/app/events/inventory_aggregate.rb b/app/events/inventory_aggregate.rb index cf1c0ebd65..e1bdb995ae 100644 --- a/app/events/inventory_aggregate.rb +++ b/app/events/inventory_aggregate.rb @@ -8,27 +8,37 @@ def on(*event_types, &block) end end - # @param organization_id + # @param organization_id [Integer] + # @param first_event [Integer] + # @param last_event [Integer] + # @param validate [Boolean] # @return [EventTypes::Inventory] - def inventory_for(organization_id) + def inventory_for(organization_id, first_event: nil, last_event: nil, validate: false) events = Event.for_organization(organization_id) + if first_event + events = events.where("id >= ?", first_event) + end + if last_event + events = events.where("id <= ?", last_event) + end inventory = EventTypes::Inventory.from(organization_id) events.group_by { |e| [e.eventable_type, e.eventable_id] }.each do |_, event_batch| - last_event = event_batch.max_by(&:event_time) - handle(last_event, inventory) + last_grouped_event = event_batch.max_by(&:updated_at) + handle(last_grouped_event, inventory, validate: validate) end inventory end # @param event [Event] # @param inventory [Inventory] - def handle(event, inventory) + # @param validate [Boolean] + def handle(event, inventory, validate: false) handler = @handlers[event.class] if handler.nil? Rails.logger.warn("No handler found for #{event.class}, skipping") return end - handler.call(event, inventory) + handler.call(event, inventory, validate: validate) end # @param payload [EventTypes::InventoryPayload] @@ -46,7 +56,6 @@ def handle_inventory_event(payload, inventory, validate: true) # @param payload [EventTypes::InventoryPayload] # @param inventory [EventTypes::Inventory] - # @param validate [Boolean] def handle_audit_event(payload, inventory) payload.items.each do |line_item| inventory.set_item_quantity(item_id: line_item.item_id, @@ -59,15 +68,15 @@ def handle_audit_event(payload, inventory) on DonationEvent, DistributionEvent, AdjustmentEvent, PurchaseEvent, TransferEvent, DistributionDestroyEvent, DonationDestroyEvent, PurchaseDestroyEvent, TransferDestroyEvent, - KitAllocateEvent, KitDeallocateEvent do |event, inventory| - handle_inventory_event(event.data, inventory, validate: false) + KitAllocateEvent, KitDeallocateEvent do |event, inventory, validate: false| + handle_inventory_event(event.data, inventory, validate: validate) end - on AuditEvent do |event, inventory| + on AuditEvent do |event, inventory, validate: false| handle_audit_event(event.data, inventory) end - on SnapshotEvent do |event, inventory| + on SnapshotEvent do |event, inventory, validate: false| inventory.storage_locations.clear inventory.storage_locations.merge!(event.data.storage_locations) end diff --git a/app/events/purchase_event.rb b/app/events/purchase_event.rb index 910c295916..ac00a67003 100644 --- a/app/events/purchase_event.rb +++ b/app/events/purchase_event.rb @@ -4,7 +4,7 @@ def self.publish(purchase) create( eventable: purchase, organization_id: purchase.organization_id, - event_time: Time.zone.now, + event_time: purchase.created_at, data: EventTypes::InventoryPayload.new( items: EventTypes::EventLineItem.from_line_items(purchase.line_items, to: purchase.storage_location_id) ) diff --git a/app/events/transfer_event.rb b/app/events/transfer_event.rb index e0343a4028..f8cf4f9624 100644 --- a/app/events/transfer_event.rb +++ b/app/events/transfer_event.rb @@ -4,7 +4,7 @@ def self.publish(transfer) create( eventable: transfer, organization_id: transfer.organization_id, - event_time: Time.zone.now, + event_time: transfer.created_at, data: EventTypes::InventoryPayload.new( items: EventTypes::EventLineItem.from_line_items(transfer.line_items, from: transfer.from.id, diff --git a/db/migrate/20231201194409_fix_event_times.rb b/db/migrate/20231201194409_fix_event_times.rb new file mode 100644 index 0000000000..f6ec957a82 --- /dev/null +++ b/db/migrate/20231201194409_fix_event_times.rb @@ -0,0 +1,9 @@ +class FixEventTimes < ActiveRecord::Migration[7.0] + def change + Event.where(type: %i(DistributionEvent DonationEvent PurchaseEvent)).find_each do |event| + next if event.eventable.nil? + + event.update_attribute(:event_time, event.eventable.created_at) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 6fa13acbfc..54906d1330 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_11_17_141301) do +ActiveRecord::Schema[7.0].define(version: 2023_12_01_194409) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/events/inventory_aggregate_spec.rb b/spec/events/inventory_aggregate_spec.rb index 8899b17d85..f310d1be94 100644 --- a/spec/events/inventory_aggregate_spec.rb +++ b/spec/events/inventory_aggregate_spec.rb @@ -447,50 +447,92 @@ end end - it "should process multiple events" do - donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) - donation.line_items << build(:line_item, quantity: 50, item: item1) - donation.line_items << build(:line_item, quantity: 30, item: item2) - DonationEvent.publish(donation) - - donation2 = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) - donation2.line_items << build(:line_item, quantity: 30, item: item1) - DonationEvent.publish(donation2) - - donation3 = FactoryBot.create(:donation, organization: organization, storage_location: storage_location2) - donation3.line_items << build(:line_item, quantity: 50, item: item2) - DonationEvent.publish(donation3) - - # correction event - donation3.line_items = [build(:line_item, quantity: 40, item: item2)] - DonationEvent.publish(donation3) - - dist = FactoryBot.create(:distribution, organization: organization, storage_location: storage_location1) - dist.line_items << build(:line_item, quantity: 10, item: item1) - DistributionEvent.publish(dist) - - dist2 = FactoryBot.create(:distribution, organization: organization, storage_location: storage_location2) - dist2.line_items << build(:line_item, quantity: 15, item: item2) - DistributionEvent.publish(dist2) - - inventory = described_class.inventory_for(organization.id) - expect(inventory).to eq(EventTypes::Inventory.new( - organization_id: organization.id, - storage_locations: { - storage_location1.id => EventTypes::EventStorageLocation.new( - id: storage_location1.id, - items: { - item1.id => EventTypes::EventItem.new(item_id: item1.id, quantity: 70), - item2.id => EventTypes::EventItem.new(item_id: item2.id, quantity: 30) - } - ), - storage_location2.id => EventTypes::EventStorageLocation.new( - id: storage_location2.id, - items: { - item2.id => EventTypes::EventItem.new(item_id: item2.id, quantity: 25) - } - ) - } - )) + describe "multiple events" do + it "should process multiple events" do + donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) + donation.line_items << build(:line_item, quantity: 50, item: item1) + donation.line_items << build(:line_item, quantity: 30, item: item2) + DonationEvent.publish(donation) + + donation2 = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) + donation2.line_items << build(:line_item, quantity: 30, item: item1) + DonationEvent.publish(donation2) + + donation3 = FactoryBot.create(:donation, organization: organization, storage_location: storage_location2) + donation3.line_items << build(:line_item, quantity: 50, item: item2) + DonationEvent.publish(donation3) + + # correction event + donation3.line_items = [build(:line_item, quantity: 40, item: item2)] + DonationEvent.publish(donation3) + + dist = FactoryBot.create(:distribution, organization: organization, storage_location: storage_location1) + dist.line_items << build(:line_item, quantity: 10, item: item1) + DistributionEvent.publish(dist) + + dist2 = FactoryBot.create(:distribution, organization: organization, storage_location: storage_location2) + dist2.line_items << build(:line_item, quantity: 15, item: item2) + DistributionEvent.publish(dist2) + + inventory = described_class.inventory_for(organization.id) + expect(inventory).to eq(EventTypes::Inventory.new( + organization_id: organization.id, + storage_locations: { + storage_location1.id => EventTypes::EventStorageLocation.new( + id: storage_location1.id, + items: { + item1.id => EventTypes::EventItem.new(item_id: item1.id, quantity: 70), + item2.id => EventTypes::EventItem.new(item_id: item2.id, quantity: 30) + } + ), + storage_location2.id => EventTypes::EventStorageLocation.new( + id: storage_location2.id, + items: { + item2.id => EventTypes::EventItem.new(item_id: item2.id, quantity: 25) + } + ) + } + )) + end + + it "should validate incorrect events" do + donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) + donation.line_items << build(:line_item, quantity: 10, item: item1) + DonationEvent.publish(donation) + + dist = FactoryBot.create(:distribution, organization: organization, storage_location: storage_location1) + dist.line_items << build(:line_item, quantity: 20, item: item1) + DistributionEvent.publish(dist) + + expect { described_class.inventory_for(organization.id, validate: true) } + .to raise_error("Could not reduce quantity by 20 for item #{item1.id} in storage location #{storage_location1.id} - current quantity is 10") + end + + it "should handle timing correctly" do + donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) + donation.line_items << build(:line_item, quantity: 30, item: item1) + DonationEvent.publish(donation) + + dist = FactoryBot.create(:distribution, organization: organization, storage_location: storage_location1) + dist.line_items << build(:line_item, quantity: 10, item: item1) + DistributionEvent.publish(dist) + + # correction event + donation.line_items[0].quantity = 20 + DonationEvent.publish(donation) + + inventory = described_class.inventory_for(organization.id, validate: true) + expect(inventory).to eq(EventTypes::Inventory.new( + organization_id: organization.id, + storage_locations: { + storage_location1.id => EventTypes::EventStorageLocation.new( + id: storage_location1.id, + items: { + item1.id => EventTypes::EventItem.new(item_id: item1.id, quantity: 10) + } + ) + } + )) + end end end