diff --git a/rails_application/app/controllers/shipments_controller.rb b/rails_application/app/controllers/shipments_controller.rb
index 297a55cba..9a93c1e48 100644
--- a/rails_application/app/controllers/shipments_controller.rb
+++ b/rails_application/app/controllers/shipments_controller.rb
@@ -2,9 +2,16 @@ class ShipmentsController < ApplicationController
def index
@shipments =
Shipments::Shipment
+ .joins(:order)
.includes(:order)
- .order("id DESC")
+ .with_full_address
+ .order(id: :desc)
.page(params[:page])
.per(10)
end
+
+ def show
+ @shipment = Shipments::Shipment.find(params[:id])
+ @shipment_items = @shipment.shipment_items.page(params[:page]).per(25)
+ end
end
diff --git a/rails_application/app/read_models/shipments/add_item_to_shipment.rb b/rails_application/app/read_models/shipments/add_item_to_shipment.rb
new file mode 100644
index 000000000..2e150283b
--- /dev/null
+++ b/rails_application/app/read_models/shipments/add_item_to_shipment.rb
@@ -0,0 +1,23 @@
+module Shipments
+ class AddItemToShipment
+ def call(event)
+ product_id = event.data.fetch(:product_id)
+ order_id = event.data.fetch(:order_id)
+ product = Orders::Product.find_by_uid!(product_id)
+
+ item = find_or_create_item(order_id, product)
+ item.quantity += 1
+ item.save!
+ end
+
+ private
+
+ def find_or_create_item(order_id, product)
+ Shipment
+ .find_or_create_by!(order_uid: order_id)
+ .shipment_items
+ .create_with(product_name: product.name, quantity: 0)
+ .find_or_create_by!(product_id: product.uid)
+ end
+ end
+end
diff --git a/rails_application/app/read_models/shipments/configuration.rb b/rails_application/app/read_models/shipments/configuration.rb
index d6f082712..4bd610b7e 100644
--- a/rails_application/app/read_models/shipments/configuration.rb
+++ b/rails_application/app/read_models/shipments/configuration.rb
@@ -7,6 +7,10 @@ class Shipment < ApplicationRecord
foreign_key: :uid,
primary_key: :order_uid
+ has_many :shipment_items
+
+ scope :with_full_address, -> { where.not(address_line_1: nil, address_line_2: nil, address_line_3: nil, address_line_4: nil) }
+
def full_address
[self.address_line_1, self.address_line_2, self.address_line_3, self.address_line_4].join(" ")
end
@@ -16,10 +20,18 @@ class Order < ApplicationRecord
self.table_name = "shipments_orders"
end
+ class ShipmentItem < ApplicationRecord
+ self.table_name = "shipment_items"
+
+ belongs_to :shipment
+ end
+
class Configuration
def call(event_store)
event_store.subscribe(SetShippingAddress, to: [Shipping::ShippingAddressAddedToShipment])
event_store.subscribe(MarkOrderPlaced, to: [Ordering::OrderPlaced])
+ event_store.subscribe(AddItemToShipment, to: [Shipping::ItemAddedToShipmentPickingList])
+ event_store.subscribe(RemoveItemFromShipment, to: [Shipping::ItemRemovedFromShipmentPickingList])
end
end
end
diff --git a/rails_application/app/read_models/shipments/remove_item_from_shipment.rb b/rails_application/app/read_models/shipments/remove_item_from_shipment.rb
new file mode 100644
index 000000000..2a4bd772c
--- /dev/null
+++ b/rails_application/app/read_models/shipments/remove_item_from_shipment.rb
@@ -0,0 +1,21 @@
+module Shipments
+ class RemoveItemFromShipment
+ def call(event)
+ product_id = event.data.fetch(:product_id)
+ order_id = event.data.fetch(:order_id)
+
+ item = find(order_id, product_id)
+ item.quantity -= 1
+ item.quantity > 0 ? item.save! : item.destroy!
+ end
+
+ private
+
+ def find(order_uid, product_id)
+ Shipment
+ .find_by!(order_uid: order_uid)
+ .shipment_items
+ .find_by!(product_id: product_id)
+ end
+ end
+end
diff --git a/rails_application/app/views/orders/show.html.erb b/rails_application/app/views/orders/show.html.erb
index 8d4188222..dc269b15d 100644
--- a/rails_application/app/views/orders/show.html.erb
+++ b/rails_application/app/views/orders/show.html.erb
@@ -35,12 +35,12 @@
"><%= @order.state %>
Shipping Details
- <% unless @shipment %>
+ <% unless @shipment&.full_address.present? %>
Shipping address is missing.
<% end %>
- <% unless @shipment %>
+ <% unless @shipment&.full_address.present? %>
<%= link_to "Add shipment address",
edit_order_shipping_address_path(@order.uid),
class: 'px-2 py-1 border rounded-md shadow-sm text-xs font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 border-transparent text-white bg-blue-600 hover:bg-blue-700'
diff --git a/rails_application/app/views/shipments/index.html.erb b/rails_application/app/views/shipments/index.html.erb
index 2d2376fae..783052424 100644
--- a/rails_application/app/views/shipments/index.html.erb
+++ b/rails_application/app/views/shipments/index.html.erb
@@ -1,26 +1,27 @@
<% content_for(:header) do %>
Shipments
<% end %>
-
+
Order Number |
Address |
+ |
-
+
<% @shipments.each do |shipment| %>
<%= link_to shipment.order.number, order_path(shipment.order.uid), class: "text-blue-500 hover:underline" %> |
<%= shipment.full_address %> |
+ <%= link_to "Show Shipment", shipment_path(shipment), class: "text-blue-500 hover:underline" %>
|
<% end %>
-
-
+
<%= page_entries_info @shipments %>
@@ -28,4 +29,4 @@
<%= paginate @shipments %>
-
\ No newline at end of file
+
diff --git a/rails_application/app/views/shipments/show.html.erb b/rails_application/app/views/shipments/show.html.erb
new file mode 100644
index 000000000..7aa2569ed
--- /dev/null
+++ b/rails_application/app/views/shipments/show.html.erb
@@ -0,0 +1,30 @@
+<% content_for(:header) do %>
+ Shipment
+ <% end %>
+
+
+ - Order Number
+ - <%= link_to @shipment.order.number, order_path(@shipment.order.uid), class: "text-blue-500 hover:underline" %>
+ - Address
+ -
+ <%= @shipment&.full_address %>
+
+
+
+
+
+
+ Product Name |
+ Quantity |
+
+
+
+
+ <% @shipment_items.each do |item| %>
+
+ <%= item.product_name %> |
+ <%= item.quantity %> |
+
+ <% end %>
+
+
diff --git a/rails_application/config/routes.rb b/rails_application/config/routes.rb
index f68854b95..2269eaac9 100644
--- a/rails_application/config/routes.rb
+++ b/rails_application/config/routes.rb
@@ -19,7 +19,7 @@
resource :invoice, only: [:create]
end
- resources :shipments, only: [:index]
+ resources :shipments, only: [:index, :show]
resources :events_catalog, only: [:index]
diff --git a/rails_application/db/migrate/20241017115056_create_shipment_items.rb b/rails_application/db/migrate/20241017115056_create_shipment_items.rb
new file mode 100644
index 000000000..635e63932
--- /dev/null
+++ b/rails_application/db/migrate/20241017115056_create_shipment_items.rb
@@ -0,0 +1,12 @@
+class CreateShipmentItems < ActiveRecord::Migration[7.2]
+ def change
+ create_table :shipment_items do |t|
+ t.references :shipment, null: false
+ t.string :product_name, null: false
+ t.integer :quantity, null: false
+ t.uuid :product_id, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/rails_application/db/migrate/20241018113912_change_order_uid_to_uuid_in_shipments.rb b/rails_application/db/migrate/20241018113912_change_order_uid_to_uuid_in_shipments.rb
new file mode 100644
index 000000000..53967d57d
--- /dev/null
+++ b/rails_application/db/migrate/20241018113912_change_order_uid_to_uuid_in_shipments.rb
@@ -0,0 +1,9 @@
+class ChangeOrderUidToUuidInShipments < ActiveRecord::Migration[7.2]
+ def up
+ change_column :shipments, :order_uid, 'uuid USING order_uid::uuid', null: false
+ end
+
+ def down
+ change_column :shipments, :order_uid, 'varchar USING order_uid::varchar', null: false
+ end
+end
diff --git a/rails_application/db/schema.rb b/rails_application/db/schema.rb
index 99d056d90..aa2604e16 100644
--- a/rails_application/db/schema.rb
+++ b/rails_application/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2024_08_27_090619) do
+ActiveRecord::Schema[7.2].define(version: 2024_10_18_113912) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -194,8 +194,18 @@
t.decimal "lowest_recent_price", precision: 8, scale: 2
end
+ create_table "shipment_items", force: :cascade do |t|
+ t.bigint "shipment_id", null: false
+ t.string "product_name", null: false
+ t.integer "quantity", null: false
+ t.uuid "product_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["shipment_id"], name: "index_shipment_items_on_shipment_id"
+ end
+
create_table "shipments", force: :cascade do |t|
- t.string "order_uid", null: false
+ t.uuid "order_uid", null: false
t.string "address_line_1"
t.string "address_line_2"
t.string "address_line_3"
diff --git a/rails_application/test/client_orders/item_removed_from_basket_test.rb b/rails_application/test/client_orders/item_removed_from_basket_test.rb
index 854354298..bfc3c06ff 100644
--- a/rails_application/test/client_orders/item_removed_from_basket_test.rb
+++ b/rails_application/test/client_orders/item_removed_from_basket_test.rb
@@ -66,6 +66,12 @@ def test_remove_item_when_quantity_eq_1
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
customer_id = SecureRandom.uuid
run_command(
diff --git a/rails_application/test/integration/shipments_test.rb b/rails_application/test/integration/shipments_test.rb
new file mode 100644
index 000000000..cbfc43422
--- /dev/null
+++ b/rails_application/test/integration/shipments_test.rb
@@ -0,0 +1,63 @@
+
+require "test_helper"
+
+class ShipmentsTest < InMemoryRESIntegrationTestCase
+ def setup
+ super
+ add_available_vat_rate(10)
+ end
+
+ def test_list_shipments
+ shopify_id = register_customer("Shopify")
+ order_id = SecureRandom.uuid
+ async_remote_id = register_product("Async Remote", 39, 10)
+
+ add_product_to_basket(order_id, async_remote_id)
+ put "/orders/#{order_id}/shipping_address",
+ params: {
+ "shipments_shipment" => {
+ address_line_1: "123 Main Street",
+ address_line_2: "Apt 1",
+ address_line_3: "San Francisco",
+ address_line_4: "US"
+ }
+ }
+ submit_order(shopify_id, order_id)
+
+ order = Orders::Order.find_by(uid: order_id)
+
+ get "/shipments"
+
+ assert_response :success
+ assert_select("td", order.number)
+ assert_select("td", "123 Main Street Apt 1 San Francisco US")
+ end
+
+ def test_shipment_page
+ shopify_id = register_customer("Shopify")
+ order_id = SecureRandom.uuid
+ async_remote_id = register_product("Async Remote", 39, 10)
+
+ add_product_to_basket(order_id, async_remote_id)
+ put "/orders/#{order_id}/shipping_address",
+ params: {
+ "shipments_shipment" => {
+ address_line_1: "123 Main Street",
+ address_line_2: "Apt 1",
+ address_line_3: "San Francisco",
+ address_line_4: "US"
+ }
+ }
+ submit_order(shopify_id, order_id)
+
+ shipment = Shipments::Shipment.find_by(order_uid: order_id)
+ order = Orders::Order.find_by(uid: order_id)
+
+ get "/shipments/#{shipment.id}"
+ assert_response :success
+ assert_select("dd", order.number)
+ assert_select("dd", "123 Main Street Apt 1 San Francisco US")
+ assert_select("td", "Async Remote")
+ assert_select("td", "1")
+ end
+end
diff --git a/rails_application/test/orders/broadcast_test.rb b/rails_application/test/orders/broadcast_test.rb
index 6e0c2df37..a4a21df49 100644
--- a/rails_application/test/orders/broadcast_test.rb
+++ b/rails_application/test/orders/broadcast_test.rb
@@ -35,6 +35,12 @@ def test_broadcast_add_item_to_basket
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
in_memory_broadcast.result.clear
@@ -63,6 +69,12 @@ def test_broadcast_remove_item_from_basket
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
order_id = SecureRandom.uuid
@@ -104,6 +116,12 @@ def test_broadcast_update_order_value
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
event_store.publish(
Pricing::PriceItemAdded.new(
@@ -164,6 +182,12 @@ def test_broadcast_update_discount
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
event_store.publish(
Pricing::PriceItemAdded.new(
diff --git a/rails_application/test/orders/item_removed_from_basket_test.rb b/rails_application/test/orders/item_removed_from_basket_test.rb
index f5d437e08..a5e84885e 100644
--- a/rails_application/test/orders/item_removed_from_basket_test.rb
+++ b/rails_application/test/orders/item_removed_from_basket_test.rb
@@ -66,6 +66,12 @@ def test_remove_item_when_quantity_eq_1
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
customer_id = SecureRandom.uuid
run_command(
diff --git a/rails_application/test/orders/order_expired_test.rb b/rails_application/test/orders/order_expired_test.rb
index 3103744a7..02a093490 100644
--- a/rails_application/test/orders/order_expired_test.rb
+++ b/rails_application/test/orders/order_expired_test.rb
@@ -17,6 +17,12 @@ def test_expire_created_order
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 39))
order_id = SecureRandom.uuid
diff --git a/rails_application/test/orders/order_placed_test.rb b/rails_application/test/orders/order_placed_test.rb
index 5ef342dc9..0d83c3b5c 100644
--- a/rails_application/test/orders/order_placed_test.rb
+++ b/rails_application/test/orders/order_placed_test.rb
@@ -14,6 +14,12 @@ def test_create_when_not_exists
product_id: product_id
)
)
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: "Async Remote"
+ )
+ )
run_command(Pricing::SetPrice.new(product_id: product_id, price: 20))
order_id = SecureRandom.uuid
order_number = Ordering::FakeNumberGenerator::FAKE_NUMBER
diff --git a/rails_application/test/shipments/item_added_to_shipment_test.rb b/rails_application/test/shipments/item_added_to_shipment_test.rb
new file mode 100644
index 000000000..e07820c33
--- /dev/null
+++ b/rails_application/test/shipments/item_added_to_shipment_test.rb
@@ -0,0 +1,96 @@
+require "test_helper"
+
+module Shipments
+ class ItemAddedToShipmentTest < InMemoryTestCase
+ cover "Shipments*"
+
+ def test_add_new_item
+ product_id = SecureRandom.uuid
+ prepare_product(product_id, "Async Remote", 49)
+
+ order_id = SecureRandom.uuid
+ item_added_to_shipment_picking_list(order_id, product_id)
+
+ assert_equal(1, ShipmentItem.count)
+
+ shipment_item = Shipment.find_by(order_uid: order_id).shipment_items.first
+
+ assert_equal(product_id, shipment_item.product_id)
+ assert_equal("Async Remote", shipment_item.product_name)
+ assert_equal(1, shipment_item.quantity)
+ end
+
+ def test_add_the_same_item_twice
+ product_id = SecureRandom.uuid
+ prepare_product(product_id, "Async Remote", 49)
+
+ order_id = SecureRandom.uuid
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_added_to_shipment_picking_list(order_id, product_id)
+
+ assert_equal(1, ShipmentItem.count)
+
+ shipment_item = Shipment.find_by(order_uid: order_id).shipment_items.first
+
+ assert_equal(product_id, shipment_item.product_id)
+ assert_equal("Async Remote", shipment_item.product_name)
+ assert_equal(2, shipment_item.quantity)
+ end
+
+ def test_add_another_item
+ product_id = SecureRandom.uuid
+ prepare_product(product_id, "Async Remote", 49)
+
+ another_product_id = SecureRandom.uuid
+ prepare_product(another_product_id, "Fearless Refactoring", 39)
+
+ order_id = SecureRandom.uuid
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_added_to_shipment_picking_list(order_id, another_product_id)
+
+ shipment = Shipment.find_by(order_uid: order_id)
+
+ assert_equal(2, shipment.shipment_items.count)
+
+ shipment_item_1 = shipment.shipment_items.find_by(product_id: product_id)
+ assert_equal("Async Remote", shipment_item_1.product_name)
+ assert_equal(1, shipment_item_1.quantity)
+
+ shipment_item_2 = shipment.shipment_items.find_by(product_id: another_product_id)
+ assert_equal("Fearless Refactoring", shipment_item_2.product_name)
+ assert_equal(1, shipment_item_2.quantity)
+ end
+
+ private
+
+ def event_store
+ Rails.configuration.event_store
+ end
+
+ def prepare_product(product_id, name, price)
+ run_command(
+ ProductCatalog::RegisterProduct.new(
+ product_id: product_id,
+ )
+ )
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: name
+ )
+ )
+ run_command(Pricing::SetPrice.new(product_id: product_id, price: price))
+ end
+
+ def item_added_to_shipment_picking_list(order_id, product_id)
+ event_store.publish(
+ Shipping::ItemAddedToShipmentPickingList.new(
+ data: {
+ order_id: order_id,
+ product_id: product_id
+ }
+ )
+ )
+ end
+ end
+end
diff --git a/rails_application/test/shipments/item_removed_from_shipment_test.rb b/rails_application/test/shipments/item_removed_from_shipment_test.rb
new file mode 100644
index 000000000..827e913ea
--- /dev/null
+++ b/rails_application/test/shipments/item_removed_from_shipment_test.rb
@@ -0,0 +1,99 @@
+require "test_helper"
+
+module Shipments
+ class ItemRemovedFromShipmentTest < InMemoryTestCase
+ cover "Shipments*"
+
+ def test_remove_item_when_quantity_is_greater_than_1
+ product_id = SecureRandom.uuid
+ prepare_product(product_id, "Async Remote", 49)
+
+ order_id = SecureRandom.uuid
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_removed_from_shipment_picking_list(order_id, product_id)
+ assert_equal(1, ShipmentItem.count)
+
+ shipment_item = Shipment.find_by(order_uid: order_id).shipment_items.first
+
+ assert_equal(shipment_item.product_id, product_id)
+ assert_equal("Async Remote", shipment_item.product_name)
+ assert_equal(1, shipment_item.quantity)
+ end
+
+ def test_remove_item_when_quantity_eq_1
+ product_id = SecureRandom.uuid
+ prepare_product(product_id, "Async Remote", 49)
+
+ order_id = SecureRandom.uuid
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_removed_from_shipment_picking_list(order_id, product_id)
+ assert_equal(0, ShipmentItem.count)
+ end
+
+ def test_remove_item_when_there_is_another_item
+ product_id = SecureRandom.uuid
+ prepare_product(product_id, "Async Remote", 49)
+
+ another_product_id = SecureRandom.uuid
+ prepare_product(another_product_id, "Fearless Refactoring", 39)
+
+ order_id = SecureRandom.uuid
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_added_to_shipment_picking_list(order_id, product_id)
+ item_added_to_shipment_picking_list(order_id, another_product_id)
+ item_removed_from_shipment_picking_list(order_id, another_product_id)
+
+ assert_equal(1, ShipmentItem.count)
+
+ shipment_item = Shipment.find_by(order_uid: order_id).shipment_items.first
+
+ assert_equal(product_id, shipment_item.product_id)
+ assert_equal("Async Remote", shipment_item.product_name)
+ assert_equal(2, shipment_item.quantity)
+ end
+
+ private
+
+ def event_store
+ Rails.configuration.event_store
+ end
+
+ def prepare_product(product_id, name, price)
+ run_command(
+ ProductCatalog::RegisterProduct.new(
+ product_id: product_id,
+ )
+ )
+ run_command(
+ ProductCatalog::NameProduct.new(
+ product_id: product_id,
+ name: name
+ )
+ )
+ run_command(Pricing::SetPrice.new(product_id: product_id, price: price))
+ end
+
+ def item_added_to_shipment_picking_list(order_id, product_id)
+ event_store.publish(
+ Shipping::ItemAddedToShipmentPickingList.new(
+ data: {
+ order_id: order_id,
+ product_id: product_id
+ }
+ )
+ )
+ end
+
+ def item_removed_from_shipment_picking_list(order_id, product_id)
+ event_store.publish(
+ Shipping::ItemRemovedFromShipmentPickingList.new(
+ data: {
+ order_id: order_id,
+ product_id: product_id
+ }
+ )
+ )
+ end
+ end
+end