diff --git a/ecommerce/fulfillment/lib/fulfillment.rb b/ecommerce/fulfillment/lib/fulfillment.rb index 50ed1ac02..03802934c 100644 --- a/ecommerce/fulfillment/lib/fulfillment.rb +++ b/ecommerce/fulfillment/lib/fulfillment.rb @@ -2,12 +2,15 @@ require_relative "fulfillment/events/order_registered" require_relative "fulfillment/events/order_confirmed" require_relative "fulfillment/events/order_cancelled" +require_relative "fulfillment/events/order_archived" require_relative "fulfillment/commands/register_order" require_relative "fulfillment/commands/confirm_order" require_relative "fulfillment/commands/cancel_order" +require_relative "fulfillment/commands/archive_order" require_relative "fulfillment/on_register_order" require_relative "fulfillment/on_cancel_order" require_relative "fulfillment/on_confirm_order" +require_relative "fulfillment/on_archive_order" require_relative "fulfillment/order" require_relative "fulfillment/number_generator" require_relative "fulfillment/fake_number_generator" @@ -22,6 +25,7 @@ def call(event_store, command_bus) command_bus.register(RegisterOrder, OnRegisterOrder.new(event_store, @number_generator.call)) command_bus.register(ConfirmOrder, OnConfirmOrder.new(event_store)) command_bus.register(CancelOrder, OnCancelOrder.new(event_store)) + command_bus.register(ArchiveOrder, OnArchiveOrder.new(event_store)) end end end diff --git a/ecommerce/fulfillment/lib/fulfillment/commands/archive_order.rb b/ecommerce/fulfillment/lib/fulfillment/commands/archive_order.rb new file mode 100644 index 000000000..034e81614 --- /dev/null +++ b/ecommerce/fulfillment/lib/fulfillment/commands/archive_order.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Fulfillment + class ArchiveOrder < Infra::Command + attribute :order_id, Infra::Types::UUID + + alias aggregate_id order_id + end +end \ No newline at end of file diff --git a/ecommerce/fulfillment/lib/fulfillment/events/order_archived.rb b/ecommerce/fulfillment/lib/fulfillment/events/order_archived.rb new file mode 100644 index 000000000..35abd1a54 --- /dev/null +++ b/ecommerce/fulfillment/lib/fulfillment/events/order_archived.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Fulfillment + class OrderArchived < Infra::Event + attribute :order_id, Infra::Types::UUID + end +end \ No newline at end of file diff --git a/ecommerce/fulfillment/lib/fulfillment/on_archive_order.rb b/ecommerce/fulfillment/lib/fulfillment/on_archive_order.rb new file mode 100644 index 000000000..d374afe27 --- /dev/null +++ b/ecommerce/fulfillment/lib/fulfillment/on_archive_order.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Fulfillment + class OnArchiveOrder + def initialize(event_store) + @repository = Infra::AggregateRootRepository.new(event_store) + end + + def call(command) + @repository.with_aggregate(Order, command.aggregate_id) do |order| + order.archive + end + end + end +end \ No newline at end of file diff --git a/ecommerce/fulfillment/lib/fulfillment/order.rb b/ecommerce/fulfillment/lib/fulfillment/order.rb index ea7cfe488..5bd30538d 100644 --- a/ecommerce/fulfillment/lib/fulfillment/order.rb +++ b/ecommerce/fulfillment/lib/fulfillment/order.rb @@ -25,6 +25,10 @@ def cancel apply OrderCancelled.new(data: { order_id: @id }) end + def archive + apply OrderArchived.new(data: { order_id: @id }) + end + on OrderRegistered do |event| @state = :new end @@ -36,5 +40,9 @@ def cancel on OrderCancelled do |event| @state = :cancelled end + + on OrderArchived do |event| + @state = :archived + end end end diff --git a/ecommerce/fulfillment/test/archive_order_test.rb b/ecommerce/fulfillment/test/archive_order_test.rb new file mode 100644 index 000000000..7d02fc710 --- /dev/null +++ b/ecommerce/fulfillment/test/archive_order_test.rb @@ -0,0 +1,46 @@ +require_relative "test_helper" + +module Fulfillment + class ArchiveOrderTest < Test + cover "Fulfillment::OnArchiveOrder*" + + def test_order_can_be_archived + aggregate_id = SecureRandom.uuid + stream = "Fulfillment::Order$#{aggregate_id}" + + arrange( + RegisterOrder.new(order_id: aggregate_id) + ) + + assert_events( + stream, + OrderArchived.new(data: { order_id: aggregate_id }) + ) { act(ArchiveOrder.new(order_id: aggregate_id)) } + end + + def test_not_registered_order_can_be_archived + aggregate_id = SecureRandom.uuid + stream = "Fulfillment::Order$#{aggregate_id}" + + assert_events( + stream, + OrderArchived.new(data: { order_id: aggregate_id }) + ) { act(ArchiveOrder.new(order_id: aggregate_id)) } + end + + def test_confirmed_order_can_be_archived + aggregate_id = SecureRandom.uuid + stream = "Fulfillment::Order$#{aggregate_id}" + + arrange( + RegisterOrder.new(order_id: aggregate_id), + ConfirmOrder.new(order_id: aggregate_id) + ) + + assert_events( + stream, + OrderArchived.new(data: { order_id: aggregate_id }) + ) { act(ArchiveOrder.new(order_id: aggregate_id)) } + end + end +end \ No newline at end of file diff --git a/ecommerce/product_catalog/lib/product_catalog.rb b/ecommerce/product_catalog/lib/product_catalog.rb index dd2048eab..3b8ced2d3 100644 --- a/ecommerce/product_catalog/lib/product_catalog.rb +++ b/ecommerce/product_catalog/lib/product_catalog.rb @@ -3,6 +3,7 @@ require_relative "product_catalog/events" require_relative "product_catalog/registration" require_relative "product_catalog/naming" +require_relative "product_catalog/archivization" module ProductCatalog @@ -10,6 +11,7 @@ class Configuration def call(event_store, command_bus) command_bus.register(RegisterProduct, Registration.new(event_store)) command_bus.register(NameProduct, Naming.new(event_store)) + command_bus.register(ArchiveProduct, Archivization.new(event_store)) end end end diff --git a/ecommerce/product_catalog/lib/product_catalog/archivization.rb b/ecommerce/product_catalog/lib/product_catalog/archivization.rb new file mode 100644 index 000000000..9c0e0b09d --- /dev/null +++ b/ecommerce/product_catalog/lib/product_catalog/archivization.rb @@ -0,0 +1,16 @@ +module ProductCatalog + class Archivization + def initialize(event_store) + @event_store = event_store + end + + def call(command) + @event_store.publish( + ProductArchived.new( + data: { product_id: command.product_id } + ), + stream_name: "ProductCatalog$#{command.product_id}" + ) + end + end +end \ No newline at end of file diff --git a/ecommerce/product_catalog/lib/product_catalog/commands.rb b/ecommerce/product_catalog/lib/product_catalog/commands.rb index 98b2ee7fe..e2b74a2eb 100644 --- a/ecommerce/product_catalog/lib/product_catalog/commands.rb +++ b/ecommerce/product_catalog/lib/product_catalog/commands.rb @@ -7,4 +7,8 @@ class NameProduct < Infra::Command attribute :product_id, Infra::Types::UUID attribute :name, Infra::Types::String end + + class ArchiveProduct < Infra::Command + attribute :product_id, Infra::Types::UUID + end end diff --git a/ecommerce/product_catalog/lib/product_catalog/events.rb b/ecommerce/product_catalog/lib/product_catalog/events.rb index 413a7b085..16380acd4 100644 --- a/ecommerce/product_catalog/lib/product_catalog/events.rb +++ b/ecommerce/product_catalog/lib/product_catalog/events.rb @@ -8,4 +8,8 @@ class ProductNamed < Infra::Event attribute :product_id, Infra::Types::String end + class ProductArchived < Infra::Event + attribute :product_id, Infra::Types::UUID + end + end diff --git a/ecommerce/product_catalog/test/archivization_test.rb b/ecommerce/product_catalog/test/archivization_test.rb new file mode 100644 index 000000000..a4371f125 --- /dev/null +++ b/ecommerce/product_catalog/test/archivization_test.rb @@ -0,0 +1,26 @@ +require_relative 'test_helper' + +module ProductCatalog + class ArchivizationTest < Test + cover "ProductCatalog*" + + def test_product_should_get_archived + uid = SecureRandom.uuid + assert archive_product(uid) + end + + def test_should_publish_event + uid = SecureRandom.uuid + product_archived = ProductCatalog::ProductArchived.new(data: { product_id: uid }) + assert_events("ProductCatalog$#{uid}", product_archived) do + archive_product(uid) + end + end + + private + + def archive_product(uid) + run_command(ArchiveProduct.new(product_id: uid)) + end + end +end \ No newline at end of file diff --git a/rails_application/app/controllers/orders_controller.rb b/rails_application/app/controllers/orders_controller.rb index d12d131d3..1f20d700b 100644 --- a/rails_application/app/controllers/orders_controller.rb +++ b/rails_application/app/controllers/orders_controller.rb @@ -1,6 +1,6 @@ class OrdersController < ApplicationController def index - @orders = Orders::Order.order("id DESC").page(params[:page]).per(10) + @orders = Orders::Order.where(archived: false).order("id DESC").page(params[:page]).per(10) end def show @@ -21,7 +21,7 @@ def edit @order_id = params[:id] @order = Orders::Order.find_by_uid(params[:id]) @order_lines = Orders::OrderLine.where(order_uid: params[:id]) - @products = Products::Product.all + @products = Products::Product.where(archived: false) @customers = Customers::Customer.all @time_promotions = TimePromotions::TimePromotion.current @@ -116,6 +116,11 @@ def cancel redirect_to root_path, notice: "Order cancelled" end + def archive + command_bus.(Fulfillment::ArchiveOrder.new(order_id: params[:id])) + redirect_to orders_path, notice: "Order was archived" + end + private def authorize_payment(order_id) diff --git a/rails_application/app/controllers/products_controller.rb b/rails_application/app/controllers/products_controller.rb index d350b2350..fc9fdb80f 100644 --- a/rails_application/app/controllers/products_controller.rb +++ b/rails_application/app/controllers/products_controller.rb @@ -16,7 +16,7 @@ class ProductForm end def index - @products = Products::Product.all + @products = Products::Product.where(archived: false) end def show @@ -76,6 +76,11 @@ def update redirect_to products_path, notice: "Product was successfully updated" end + def archive + command_bus.(ProductCatalog::ArchiveProduct.new(product_id: params[:id])) + redirect_to products_path, notice: "Product was archived" + end + private def create_product(product_id, name) diff --git a/rails_application/app/read_models/orders/archive_order.rb b/rails_application/app/read_models/orders/archive_order.rb new file mode 100644 index 000000000..b7e3dbcbf --- /dev/null +++ b/rails_application/app/read_models/orders/archive_order.rb @@ -0,0 +1,8 @@ +module Orders + class ArchiveOrder + def call(event) + order = Order.find_by(uid: event.data[:order_id]) + order&.update!(archived: true) + end + end +end \ No newline at end of file diff --git a/rails_application/app/read_models/orders/configuration.rb b/rails_application/app/read_models/orders/configuration.rb index 759662d11..d348749fb 100644 --- a/rails_application/app/read_models/orders/configuration.rb +++ b/rails_application/app/read_models/orders/configuration.rb @@ -48,6 +48,7 @@ def call(event_store) event_store.subscribe(CancelOrder.new, to: [Fulfillment::OrderCancelled]) event_store.subscribe(UpdateTimePromotionDiscountValue.new, to: [Pricing::PercentageDiscountSet]) event_store.subscribe(RemoveTimePromotionDiscount.new, to: [Pricing::PercentageDiscountRemoved]) + event_store.subscribe(ArchiveOrder.new, to: [Fulfillment::OrderArchived]) subscribe( ->(event) { broadcast_order_state_change(event.data.fetch(:order_id), 'Submitted') }, diff --git a/rails_application/app/read_models/products/archive_product.rb b/rails_application/app/read_models/products/archive_product.rb new file mode 100644 index 000000000..9e038f8a7 --- /dev/null +++ b/rails_application/app/read_models/products/archive_product.rb @@ -0,0 +1,8 @@ +module Products + class ArchiveProduct + def call(event) + product = Product.find_by(id: event.data[:product_id]) + product&.update!(archived: true) + end + end +end \ No newline at end of file diff --git a/rails_application/app/read_models/products/configuration.rb b/rails_application/app/read_models/products/configuration.rb index 3666b00ff..0b4bcbe67 100644 --- a/rails_application/app/read_models/products/configuration.rb +++ b/rails_application/app/read_models/products/configuration.rb @@ -54,6 +54,7 @@ def call @read_model.subscribe_copy(Inventory::StockLevelChanged, :stock_level) @read_model.subscribe_copy(Taxes::VatRateSet, [:vat_rate, :code]) @read_model.subscribe_copy(Inventory::AvailabilityChanged, :available) + @event_store.subscribe(ArchiveProduct.new, to: [ProductCatalog::ProductArchived]) @event_store.subscribe(RefreshFuturePricesCalendar, to: [Pricing::PriceSet]) end end diff --git a/rails_application/app/views/orders/show.html.erb b/rails_application/app/views/orders/show.html.erb index 0f46fcbce..5b3f9992b 100644 --- a/rails_application/app/views/orders/show.html.erb +++ b/rails_application/app/views/orders/show.html.erb @@ -30,6 +30,11 @@ <% if (@order.state == "Submitted") %> <%= button_to("Cancel Order", cancel_order_path(@order.uid), class: "inline-flex items-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-50 border-gray-300 text-gray-700 bg-white hover:bg-gray-50") %> <% end %> + + <%= button_to 'Archive', archive_order_path(@order.uid), + method: :post, + data: { confirm: 'Are you sure?' }, + class: 'ml-3 inline-flex items-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 border-gray-300 text-gray-700 bg-white hover:bg-gray-50' %> <% end %>
diff --git a/rails_application/app/views/products/show.html.erb b/rails_application/app/views/products/show.html.erb index 56a55e6d0..b6c341860 100644 --- a/rails_application/app/views/products/show.html.erb +++ b/rails_application/app/views/products/show.html.erb @@ -14,6 +14,11 @@ <%= primary_action_button do %> <%= link_to 'Edit Product', edit_product_path(@product.id) %> <% end %> + + <%= button_to 'Archive', archive_product_path(@product.id), + method: :post, + data: { confirm: 'Are you sure?' }, + class: 'sm:ml-3 inline-flex items-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 border-gray-300 text-gray-700 bg-white hover:bg-gray-50' %> <% end %>
diff --git a/rails_application/config/routes.rb b/rails_application/config/routes.rb index 4cb621fe4..5e2836cc5 100644 --- a/rails_application/config/routes.rb +++ b/rails_application/config/routes.rb @@ -10,6 +10,7 @@ post :remove_item post :pay post :cancel + post :archive get :edit_discount post :update_discount post :remove_discount @@ -34,6 +35,7 @@ resources :products, only: [:new, :show, :create, :index, :edit, :update] do resources :supplies, only: [:new, :create] member do + post :archive post :add_future_price, to: "product/future_price#add_future_price", as: "add_future_price" end end diff --git a/rails_application/db/migrate/20250915083846_add_archived_to_products.rb b/rails_application/db/migrate/20250915083846_add_archived_to_products.rb new file mode 100644 index 000000000..edf6609af --- /dev/null +++ b/rails_application/db/migrate/20250915083846_add_archived_to_products.rb @@ -0,0 +1,5 @@ +class AddArchivedToProducts < ActiveRecord::Migration[7.2] + def change + add_column :products, :archived, :boolean, default: false, null: false + end +end diff --git a/rails_application/db/migrate/20250915083902_add_archived_to_orders.rb b/rails_application/db/migrate/20250915083902_add_archived_to_orders.rb new file mode 100644 index 000000000..1bbfdc9e5 --- /dev/null +++ b/rails_application/db/migrate/20250915083902_add_archived_to_orders.rb @@ -0,0 +1,5 @@ +class AddArchivedToOrders < ActiveRecord::Migration[7.2] + def change + add_column :orders, :archived, :boolean, default: false, null: false + end +end diff --git a/rails_application/db/schema.rb b/rails_application/db/schema.rb index db3b35d91..48b3c4a01 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: 2025_09_08_135920) do +ActiveRecord::Schema[7.2].define(version: 2025_09_15_083902) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -173,6 +173,7 @@ t.decimal "time_promotion_discount_value", precision: 8, scale: 2 t.datetime "total_value_updated_at" t.datetime "discount_updated_at" + t.boolean "archived", default: false, null: false t.index ["uid"], name: "index_orders_on_uid", unique: true end @@ -194,6 +195,7 @@ t.string "vat_rate_code" t.text "current_prices_calendar" t.integer "available" + t.boolean "archived", default: false, null: false end create_table "public_offer_products", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|