Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/distributions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def calendar
def picked_up
distribution = current_organization.distributions.find(params[:id])

if !distribution.complete? && distribution.complete!
if DistributionCompleteService.new(distribution.id).call.success?
flash[:notice] = 'This distribution has been marked as being completed!'
else
flash[:error] = 'Sorry, we encountered an error when trying to mark this distribution as being completed'
Expand Down
17 changes: 17 additions & 0 deletions app/events/distribution_complete_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class DistributionCompleteEvent < Event
serialize :data, coder: EventTypes::StructCoder.new(EventTypes::DistributionPayload)

# @param distribution [Distribution]
def self.publish(distribution)
create(
eventable: distribution,
group_id: "dist-complete-#{distribution.id}-#{SecureRandom.hex}",
organization_id: distribution.organization_id,
event_time: Time.zone.now,
data: EventTypes::DistributionPayload.new(
reserves_inventory: false,
items: EventTypes::EventLineItem.from_line_items(distribution.line_items, from: distribution.storage_location_id)
)
)
end
end
5 changes: 4 additions & 1 deletion app/events/distribution_destroy_event.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
class DistributionDestroyEvent < Event
serialize :data, coder: EventTypes::StructCoder.new(EventTypes::DistributionPayload)

# @param distribution [Distribution]
def self.publish(distribution)
create(
eventable: distribution,
group_id: "dist-destroy-#{distribution.id}-#{SecureRandom.hex}",
organization_id: distribution.organization_id,
event_time: Time.zone.now,
data: EventTypes::InventoryPayload.new(
data: EventTypes::DistributionPayload.new(
reserves_inventory: false,
items: EventTypes::EventLineItem.zeroed_line_items(distribution.line_items, from: distribution.storage_location_id)
)
)
Expand Down
5 changes: 4 additions & 1 deletion app/events/distribution_event.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
class DistributionEvent < Event
serialize :data, coder: EventTypes::StructCoder.new(EventTypes::DistributionPayload)

# @param distribution [Distribution]
def self.publish(distribution)
create(
eventable: distribution,
group_id: "dist-#{distribution.id}-#{SecureRandom.hex}",
organization_id: distribution.organization_id,
event_time: Time.zone.now,
data: EventTypes::InventoryPayload.new(
data: EventTypes::DistributionPayload.new(
reserves_inventory: Flipper.enabled?(:reserved_inventory, distribution.organization) && distribution.scheduled?,
items: EventTypes::EventLineItem.from_line_items(distribution.line_items, from: distribution.storage_location_id)
)
)
Expand Down
9 changes: 9 additions & 0 deletions app/events/event_types/distribution_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Types
include Dry.Types()
end

module EventTypes
class DistributionPayload < InventoryPayload
attribute :reserves_inventory, Types::Bool.default(false)
end
end
5 changes: 5 additions & 0 deletions app/events/event_types/event_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ class EventItem < Dry::Struct
transform_keys(&:to_sym)
attribute :item_id, Types::Integer
attribute :quantity, Types::Integer
attribute :reserved_quantity, Types::Integer.default(0)
attribute? :storage_location_id, Types::Integer

def physical_quantity
quantity + reserved_quantity
end
end
end
39 changes: 34 additions & 5 deletions app/events/event_types/event_storage_location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def reset!
# @param item_id [Integer]
# @param quantity [Integer]
def set_inventory(item_id, quantity)
items[item_id] = EventTypes::EventItem.new(item_id: item_id, quantity: quantity, storage_location_id: id)
items[item_id] = EventTypes::EventItem.new(
item_id: item_id,
storage_location_id: id,
quantity: quantity,
reserved_quantity: items[item_id]&.reserved_quantity || 0
)
end

# @param item_id [Integer]
Expand All @@ -38,18 +43,42 @@ def reduce_inventory(item_id, quantity, validate: true)
end
end
current_quantity = items[item_id]&.quantity || 0
items[item_id] = EventTypes::EventItem.new(item_id: item_id,
items[item_id] = EventTypes::EventItem.new(
item_id: item_id,
storage_location_id: id,
quantity: current_quantity - quantity)
quantity: current_quantity - quantity,
reserved_quantity: items[item_id]&.reserved_quantity || 0
)
end

# @param item_id [Integer]
# @param quantity [Integer]
def add_inventory(item_id, quantity)
current_quantity = items[item_id]&.quantity || 0
items[item_id] = EventTypes::EventItem.new(item_id: item_id,
items[item_id] = EventTypes::EventItem.new(
item_id: item_id,
storage_location_id: id,
quantity: current_quantity + quantity,
reserved_quantity: items[item_id]&.reserved_quantity || 0
)
end

# @param item_id [Integer]
# @param quantity [Integer] positive to reserve, negative to release
# @param validate [Boolean]
def adjust_reserved(item_id, quantity, validate: true)
current_quantity = items[item_id]&.reserved_quantity || 0
if validate && (current_quantity + quantity).negative?
raise InventoryActionError.new("Could not reduce reserved quantity by #{-quantity} - current reserved quantity is #{current_quantity}",
item_id,
id)
end
items[item_id] = EventTypes::EventItem.new(
item_id: item_id,
storage_location_id: id,
quantity: current_quantity + quantity)
quantity: items[item_id]&.quantity || 0,
reserved_quantity: current_quantity + quantity
)
end
end
end
9 changes: 9 additions & 0 deletions app/events/event_types/inventory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ def set_item_quantity(item_id:, quantity:, location:)
storage_locations[location].set_inventory(item_id, quantity)
end

# @param item_id [Integer]
# @param quantity [Integer] positive to reserve, negative to release
# @param location [Integer]
# @param validate [Boolean]
def adjust_reserved_item(item_id:, quantity:, location:, validate: true)
storage_locations[location] ||= EventTypes::EventStorageLocation.new(id: location, items: {})
storage_locations[location].adjust_reserved(item_id, quantity, validate: validate)
end

# @param item_id [Integer]
# @param quantity [Integer]
# @param from_location [Integer]
Expand Down
41 changes: 35 additions & 6 deletions app/events/inventory_aggregate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,23 @@ def handle_inventory_event(payload, inventory, validate: true, previous_event: n
errors = []
payload.items.each do |line_item|
quantity = line_item.quantity
if previous_event
previous_item = previous_event.data.items.find { |i| i.same_item?(line_item) }
quantity -= previous_item.quantity if previous_item
end
previous_item = previous_event&.data&.items&.find { |i| i.same_item?(line_item) }
quantity -= previous_item.quantity if previous_item
move_item(inventory: inventory,
item_id: line_item.item_id,
quantity: quantity,
from_location: line_item.from_storage_location,
to_location: line_item.to_storage_location,
validate: validate,
errors: errors)
reserved = reserves?(payload) ? line_item.quantity : 0
reserved -= previous_item.quantity if previous_item && reserves?(previous_event.data)
adjust_reserved(inventory: inventory,
item_id: line_item.item_id,
quantity: reserved,
location: line_item.from_storage_location,
validate: validate,
errors: errors)
end
# remove the quantity from any items that are now missing
previous_event&.data&.items&.each do |previous_item|
Expand All @@ -92,6 +98,12 @@ def handle_inventory_event(payload, inventory, validate: true, previous_event: n
to_location: previous_item.from_storage_location,
validate: validate,
errors: errors)
adjust_reserved(inventory: inventory,
item_id: previous_item.item_id,
quantity: reserves?(previous_event.data) ? -previous_item.quantity : 0,
location: previous_item.from_storage_location,
validate: validate,
errors: errors)
end
end

Expand All @@ -108,6 +120,23 @@ def handle_audit_event(payload, inventory)
end
end

# @param payload [EventTypes::InventoryPayload]
# @return [Boolean]
def reserves?(payload)
payload.respond_to?(:reserves_inventory) && payload.reserves_inventory
end

def adjust_reserved(inventory:, item_id:, quantity:, location:, validate:, errors:)
return if quantity.zero? || location.nil?

inventory.adjust_reserved_item(item_id: item_id, quantity: quantity, location: location, validate: validate)
rescue InventoryActionError => e
item = Item.find_by(id: e.item_id)&.name || "Item ID #{e.item_id}"
loc = StorageLocation.find_by(id: e.storage_location_id)&.name || "Storage Location ID #{e.storage_location_id}"
e.message << " for #{item} in #{loc}"
errors.push(e)
end

def move_item(inventory:, item_id:, quantity:, from_location:, to_location:, validate:, errors:)
inventory.move_item(item_id: item_id,
quantity: quantity,
Expand All @@ -132,8 +161,8 @@ def move_item(inventory:, item_id:, quantity:, from_location:, to_location:, val

# diff previous event
on DonationEvent, DistributionEvent, AdjustmentEvent, PurchaseEvent,
TransferEvent, DistributionDestroyEvent, DonationDestroyEvent,
PurchaseDestroyEvent, TransferDestroyEvent,
TransferEvent, DistributionDestroyEvent, DistributionCompleteEvent,
DonationDestroyEvent, PurchaseDestroyEvent, TransferDestroyEvent,
UpdateExistingEvent do |event, inventory, validate: false, previous_event: nil|
handle_inventory_event(event.data, inventory, validate: validate, previous_event: previous_event)
rescue InventoryError => e
Expand Down
1 change: 1 addition & 0 deletions app/models/view/inventory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def load_item_details
item_id: item.item_id,
storage_location_id: loc.id,
quantity: item.quantity,
reserved_quantity: item.reserved_quantity,
db_item: db_item
)
false
Expand Down
14 changes: 14 additions & 0 deletions app/services/distribution_complete_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class DistributionCompleteService < DistributionService
def initialize(distribution_id)
@distribution_id = distribution_id
end

def call
perform_distribution_service do
raise "Distribution #{distribution_id} is already complete" if distribution.complete?

DistributionCompleteEvent.publish(distribution)
distribution.complete!
end
end
end
39 changes: 39 additions & 0 deletions spec/events/distribution_event_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
RSpec.describe DistributionEvent do
let(:organization) { create(:organization) }
let(:storage_location) { create(:storage_location, organization: organization) }
let(:item) { create(:item, organization: organization) }
let(:distribution) do
dist = create(:distribution, organization: organization, storage_location: storage_location)
dist.line_items << build(:line_item, quantity: 30, item: item, itemizable: dist)
dist
end

before { TestInventory.create_inventory(organization, {storage_location.id => {item.id => 100}}) }

describe ".publish" do
subject { described_class.publish(distribution).data.reserves_inventory }

context "when the feature is enabled for the organization" do
before { Flipper.enable(:reserved_inventory) }

it { is_expected.to be true }

context "when the distribution is already complete" do
before do
distribution.complete!
end

it { is_expected.to be false }
end

end

context "when the feature is not enabled for the organization" do
before do
expect(Flipper.enabled?(:reserved_inventory)).to eq false
end

it { is_expected.to be false }
end
end
end
26 changes: 26 additions & 0 deletions spec/events/event_types/event_item_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
RSpec.describe EventTypes::EventItem do
let(:quantity) { 70 }
let(:reserved_quantity) { 30 }
subject(:base_item) { described_class.new(item_id: 1, **{ quantity: quantity, reserved_quantity: reserved_quantity }.compact_blank) }

describe "#physical_quantity" do
subject { base_item.physical_quantity }

it "sums available and reserved" do
expect(subject).to eq(quantity + reserved_quantity)
end
end

describe "#reserved_quantity" do
subject { base_item.reserved_quantity }

context "when no reserve quantity is provided" do
let(:reserved_quantity) { nil }

it "defaults to zero so payloads predating the attribute still load" do
expect(subject).to eq(0)
expect(base_item.physical_quantity).to eq(70)
end
end
end
end
Loading
Loading