From ca5941d3aa32107203aefb0cda6a2048dc5cb288 Mon Sep 17 00:00:00 2001 From: marlena-b Date: Mon, 21 Oct 2024 17:17:40 +0200 Subject: [PATCH] Display shipment items on shipment page --- .../app/controllers/shipments_controller.rb | 9 +- .../shipments/add_item_to_shipment.rb | 23 +++++ .../read_models/shipments/configuration.rb | 12 +++ .../shipments/remove_item_from_shipment.rb | 21 ++++ .../app/views/orders/show.html.erb | 4 +- .../app/views/shipments/index.html.erb | 11 ++- .../app/views/shipments/show.html.erb | 30 ++++++ rails_application/config/routes.rb | 2 +- .../20241017115056_create_shipment_items.rb | 12 +++ ...2_change_order_uid_to_uuid_in_shipments.rb | 9 ++ rails_application/db/schema.rb | 14 ++- .../item_removed_from_basket_test.rb | 6 ++ .../test/integration/shipments_test.rb | 63 ++++++++++++ .../test/orders/broadcast_test.rb | 24 +++++ .../orders/item_removed_from_basket_test.rb | 6 ++ .../test/orders/order_expired_test.rb | 6 ++ .../test/orders/order_placed_test.rb | 6 ++ .../shipments/item_added_to_shipment_test.rb | 96 ++++++++++++++++++ .../item_removed_from_shipment_test.rb | 99 +++++++++++++++++++ 19 files changed, 442 insertions(+), 11 deletions(-) create mode 100644 rails_application/app/read_models/shipments/add_item_to_shipment.rb create mode 100644 rails_application/app/read_models/shipments/remove_item_from_shipment.rb create mode 100644 rails_application/app/views/shipments/show.html.erb create mode 100644 rails_application/db/migrate/20241017115056_create_shipment_items.rb create mode 100644 rails_application/db/migrate/20241018113912_change_order_uid_to_uuid_in_shipments.rb create mode 100644 rails_application/test/integration/shipments_test.rb create mode 100644 rails_application/test/shipments/item_added_to_shipment_test.rb create mode 100644 rails_application/test/shipments/item_removed_from_shipment_test.rb 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 %> - + + - + <% @shipments.each do |shipment| %> + <% end %>
Order Number Address
<%= 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" %>
- - + 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 %> +
+
+ + + + + + + + + + + <% @shipment_items.each do |item| %> + + + + + <% end %> + +
Product NameQuantity
<%= item.product_name %><%= item.quantity %>
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