From 0517f2930f1d608d110d4d2899543271f83a5464 Mon Sep 17 00:00:00 2001 From: Madeline Collier Date: Tue, 17 Dec 2024 11:07:27 +0100 Subject: [PATCH] Add new admin store credits create flow --- .../store_credits/index/component.html.erb | 16 ++- .../users/store_credits/index/component.js | 15 +++ .../users/store_credits/index/component.rb | 6 ++ .../store_credits/new/component.html.erb | 31 ++++++ .../users/store_credits/new/component.rb | 23 ++++ .../users/store_credits/new/component.yml | 5 + .../solidus_admin/store_credits_controller.rb | 76 ++++++++++++- admin/config/routes.rb | 2 +- admin/spec/features/store_credits_spec.rb | 36 ++++++- .../solidus_admin/store_credits_spec.rb | 100 ++++++++++++++++++ 10 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 admin/app/components/solidus_admin/users/store_credits/index/component.js create mode 100644 admin/app/components/solidus_admin/users/store_credits/new/component.html.erb create mode 100644 admin/app/components/solidus_admin/users/store_credits/new/component.rb create mode 100644 admin/app/components/solidus_admin/users/store_credits/new/component.yml diff --git a/admin/app/components/solidus_admin/users/store_credits/index/component.html.erb b/admin/app/components/solidus_admin/users/store_credits/index/component.html.erb index 5d70c4be2b3..d5eefee139e 100644 --- a/admin/app/components/solidus_admin/users/store_credits/index/component.html.erb +++ b/admin/app/components/solidus_admin/users/store_credits/index/component.html.erb @@ -4,7 +4,11 @@ <%= page_header_title(t(".title", email: @user.email)) %> <%= page_header_actions do %> - <%= render component("ui/button").new(tag: :a, text: t(".add_store_credit"), href: spree.new_admin_user_store_credit_url(user_id: @user.id, only_path: true)) %> + <%= render component("ui/button").new( + "data-action": "click->#{stimulus_id}#actionButtonClicked", + "data-#{stimulus_id}-url-param": solidus_admin.new_user_store_credit_path(user_id: @user.id, _turbo_frame: :new_store_credit_modal), + text: t(".add_store_credit"), + )%> <% end %> <% end %> @@ -37,7 +41,11 @@ <% else %> <%= render component('ui/panel').new(title: t(".store_credit")) do %> <%= t(".no_credits_found") %> - <%= render component("ui/button").new(tag: :a, text: t(".create_one"), href: spree.new_admin_user_store_credit_url(user_id: @user.id, only_path: true)) %> + <%= render component("ui/button").new( + "data-action": "click->#{stimulus_id}#actionButtonClicked", + "data-#{stimulus_id}-url-param": solidus_admin.new_user_store_credit_path(user_id: @user.id, _turbo_frame: :new_store_credit_modal), + text: t(".create_one"), + )%> <% end %> <% end %> <% end %> @@ -46,4 +54,8 @@ <%= render component("users/stats").new(user: @user) %> <% end %> <% end %> + + <% turbo_frames.each do |frame| %> + <%= turbo_frame_tag frame %> + <% end %> <% end %> diff --git a/admin/app/components/solidus_admin/users/store_credits/index/component.js b/admin/app/components/solidus_admin/users/store_credits/index/component.js new file mode 100644 index 00000000000..b6f7437145d --- /dev/null +++ b/admin/app/components/solidus_admin/users/store_credits/index/component.js @@ -0,0 +1,15 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + actionButtonClicked(event) { + const url = new URL(event.params.url, "http://dummy.com") + const params = new URLSearchParams(url.search) + const frameId = params.get('_turbo_frame') + const frame = frameId ? { frame: frameId } : {} + // remove the custom _turbo_frame param from url search: + params.delete('_turbo_frame') + url.search = params.toString() + + window.Turbo.visit(url.pathname + url.search, frame) + } +} diff --git a/admin/app/components/solidus_admin/users/store_credits/index/component.rb b/admin/app/components/solidus_admin/users/store_credits/index/component.rb index 615a2163eda..6e961826a15 100644 --- a/admin/app/components/solidus_admin/users/store_credits/index/component.rb +++ b/admin/app/components/solidus_admin/users/store_credits/index/component.rb @@ -42,6 +42,12 @@ def tabs ] end + def turbo_frames + %w[ + new_store_credit_modal + ] + end + def rows @store_credits end diff --git a/admin/app/components/solidus_admin/users/store_credits/new/component.html.erb b/admin/app/components/solidus_admin/users/store_credits/new/component.html.erb new file mode 100644 index 00000000000..d90ebf1c124 --- /dev/null +++ b/admin/app/components/solidus_admin/users/store_credits/new/component.html.erb @@ -0,0 +1,31 @@ +<%= turbo_frame_tag :new_store_credit_modal do %> + <%= render component("ui/modal").new(title: t(".title")) do |modal| %> + <%= form_for @store_credit, url: solidus_admin.user_store_credits_path(@user), method: :post, html: { id: form_id } do |f| %> +
+ <%= render component("ui/forms/field").text_field(f, :amount, class: "required") %> + <%= render component("ui/forms/field").select( + f, + :currency, + currency_select_options.html_safe, + include_blank: t("spree.currency"), + html: { required: true } + ) %> + <%= render component("ui/forms/field").select( + f, + :category_id, + store_credit_categories_select_options.html_safe, + include_blank: t("spree.category"), + html: { required: true } + ) %> + <%= render component("ui/forms/field").text_field(f, :memo) %> +
+ <% modal.with_actions do %> +
+ <%= render component("ui/button").new(scheme: :secondary, text: t(".cancel")) %> +
+ <%= render component("ui/button").new(form: form_id, type: :submit, text: t(".submit")) %> + <% end %> + <% end %> + <% end %> +<% end %> +<%= render component("users/store_credits/index").new(user: @user, store_credits: @store_credits) %> diff --git a/admin/app/components/solidus_admin/users/store_credits/new/component.rb b/admin/app/components/solidus_admin/users/store_credits/new/component.rb new file mode 100644 index 00000000000..afecb8b683e --- /dev/null +++ b/admin/app/components/solidus_admin/users/store_credits/new/component.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class SolidusAdmin::Users::StoreCredits::New::Component < SolidusAdmin::BaseComponent + def initialize(user:, store_credit:, categories:) + @user = user + @store_credit = store_credit + @store_credit_categories = categories + @store_credits = Spree::StoreCredit.where(user_id: @user.id).order(id: :desc) + end + + def form_id + dom_id(@store_credit, "#{stimulus_id}_new_form") + end + + def currency_select_options + options_from_collection_for_select(Spree::Config.available_currencies, :iso_code, :iso_code, Spree::Config.currency) + end + + def store_credit_categories_select_options + # Placeholder + Store Credit Categories + "" + options_from_collection_for_select(@store_credit_categories, :id, :name) + end +end diff --git a/admin/app/components/solidus_admin/users/store_credits/new/component.yml b/admin/app/components/solidus_admin/users/store_credits/new/component.yml new file mode 100644 index 00000000000..9b868b52116 --- /dev/null +++ b/admin/app/components/solidus_admin/users/store_credits/new/component.yml @@ -0,0 +1,5 @@ +en: + title: New Store Credit + cancel: Cancel + submit: Create + choose_category: Choose Store Credit Category diff --git a/admin/app/controllers/solidus_admin/store_credits_controller.rb b/admin/app/controllers/solidus_admin/store_credits_controller.rb index 02204834219..3cf165e3df8 100644 --- a/admin/app/controllers/solidus_admin/store_credits_controller.rb +++ b/admin/app/controllers/solidus_admin/store_credits_controller.rb @@ -6,6 +6,7 @@ class StoreCreditsController < SolidusAdmin::BaseController before_action :set_store_credit, only: [:show, :edit_amount, :update_amount, :edit_memo, :update_memo, :edit_validity, :invalidate] before_action :set_store_credit_reasons, only: [:edit_amount, :update_amount, :edit_validity, :invalidate] before_action :set_store_credit_events, only: [:show, :edit_amount, :edit_memo, :edit_validity] + before_action :set_store_credit_categories, only: [:new] def index @store_credits = Spree::StoreCredit.where(user_id: @user.id).order(id: :desc) @@ -21,6 +22,48 @@ def show end end + def new + @store_credit ||= Spree::StoreCredit.new + + respond_to do |format| + format.html { + render component("users/store_credits/new").new( + user: @user, + store_credit: @store_credit, + categories: @store_credit_categories + ) + } + end + end + + def create + @store_credit = @user.store_credits.build( + permitted_store_credit_params.merge({ + created_by: spree_current_user, + action_originator: spree_current_user + }) + ) + + return unless ensure_amount { render_new_with_errors } + return unless ensure_store_credit_category { render_new_with_errors } + + if @store_credit.save + respond_to do |format| + flash[:notice] = t('.success') + + format.html do + redirect_to solidus_admin.user_store_credits_path(@user), status: :see_other + end + + format.turbo_stream do + render turbo_stream: '' + end + end + else + render_new_with_errors + end + end + def edit_amount respond_to do |format| format.html { @@ -135,19 +178,39 @@ def set_store_credit_reasons @store_credit_reasons = Spree::StoreCreditReason.active.order(:name) end + def set_store_credit_categories + @store_credit_categories = Spree::StoreCreditCategory.all.order(:name) + end + def set_store_credit_events @store_credit_events = @store_credit.store_credit_events.chronological end def permitted_store_credit_params permitted_params = [:amount, :currency, :category_id, :memo] + permitted_params << :category_id if action_name.to_sym == :create permitted_params << :store_credit_reason_id if [:update_amount, :invalidate].include?(action_name.to_sym) params.require(:store_credit).permit(permitted_params).merge(created_by: spree_current_user) end + def render_new_with_errors + set_store_credit_categories + + respond_to do |format| + format.html do + render component("users/store_credits/new").new( + user: @user, + store_credit: @store_credit, + categories: @store_credit_categories + ), + status: :unprocessable_entity + end + end + end + def render_edit_with_errors - @store_credit_events = @store_credit.store_credit_events.chronological + set_store_credit_events template = if action_name.to_sym == :invalidate "edit_validity" @@ -187,5 +250,16 @@ def ensure_store_credit_reason end true end + + def ensure_store_credit_category + @store_credit_category = Spree::StoreCreditCategory.find_by(id: permitted_store_credit_params[:category_id]) + + if @store_credit_category.blank? + @store_credit.errors.add(:category_id, "Store Credit category must be provided") + yield if block_given? # Block is for error template rendering on a per-action basis so this can be re-used. + return false + end + true + end end end diff --git a/admin/config/routes.rb b/admin/config/routes.rb index ffa0223158f..4f70c26e766 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -53,7 +53,7 @@ get :items end - resources :store_credits, only: [:index, :show], constraints: { id: /\d+/ }, controller: "store_credits" do + resources :store_credits, only: [:index, :show, :new, :create], controller: "store_credits" do member do get :edit_amount put :update_amount diff --git a/admin/spec/features/store_credits_spec.rb b/admin/spec/features/store_credits_spec.rb index 462e9d12c17..27788b13e31 100644 --- a/admin/spec/features/store_credits_spec.rb +++ b/admin/spec/features/store_credits_spec.rb @@ -4,6 +4,9 @@ describe "StoreCredits", :js, type: :feature do let(:admin) { create(:admin_user, email: "admin@example.com") } + let!(:store_credit_reason) { create(:store_credit_reason, name: "credit given in error") } + let!(:store_credit_category) { create(:store_credit_category, name: "Gift Card") } + let!(:store_credit_type) { create(:primary_credit_type) } before do sign_in admin @@ -27,11 +30,40 @@ it "shows the appropriate content" do expect(page).to have_content("No Store Credits found.") end + + it "allows creation of a new store credit" do + click_on "Create One" + + expect(page).to have_selector("dialog", wait: 5) + expect(page).to have_content("New Store Credit") + + within("dialog") do + fill_in "Amount", with: "" + select "Gift Card", from: "store_credit[category_id]" + click_on "Create" + expect(page).to have_content("must be greater than 0") + click_on "Cancel" + end + + click_on "Create One" + + expect(page).to have_selector("dialog", wait: 5) + expect(page).to have_content("New Store Credit") + + within("dialog") do + fill_in "Amount", with: "666.66" + select "Gift Card", from: "store_credit[category_id]" + fill_in "Memo", with: "A brand new store credit, how nice!" + click_on "Create" + end + + expect(page).to have_content("Store credit was successfully created.") + expect(page).to have_content("Current balance: $666.66") + end end context "when a user has store credits" do let!(:store_credit) { create(:store_credit, amount: 199.00, currency: "USD") } - let!(:store_credit_reason) { create(:store_credit_reason, name: "credit given in error") } before do store_credit.user.update(email: "customer@example.com") @@ -61,8 +93,6 @@ end context "when clicking through to a single store credit" do - let!(:store_credit_reason) { create(:store_credit_reason, name: "credit given in error") } - before do stub_authorization!(admin) find_row("$199.00").click diff --git a/admin/spec/requests/solidus_admin/store_credits_spec.rb b/admin/spec/requests/solidus_admin/store_credits_spec.rb index e691bde5c18..7e4f7a951f5 100644 --- a/admin/spec/requests/solidus_admin/store_credits_spec.rb +++ b/admin/spec/requests/solidus_admin/store_credits_spec.rb @@ -11,6 +11,38 @@ let(:invalid_params) { { amount: nil } } let(:valid_memo_params) { { memo: "Updated memo text" } } let(:invalid_reason_params) { { store_credit_reason_id: nil } } + let(:valid_create_params) do + { + store_credit: { + amount: 150, + currency: "USD", + category_id: create(:store_credit_category).id, + memo: "Initial store credit" + } + } + end + + let(:invalid_create_amount_params) do + { + store_credit: { + amount: nil, + currency: "USD", + category_id: create(:store_credit_category).id, + memo: "Invalid store credit" + } + } + end + + let(:invalid_create_category_params) do + { + store_credit: { + amount: 100, + currency: "USD", + category_id: nil, + memo: "Invalid store credit" + } + } + end before do allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) @@ -24,6 +56,65 @@ end end + describe "GET /new" do + it "renders the new store credit template with a 200 OK status" do + get solidus_admin.new_user_store_credit_path(user) + expect(response).to have_http_status(:ok) + expect(response.body).to include(store_credit.amount.to_s) + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new store credit" do + expect { + post solidus_admin.user_store_credits_path(user), params: valid_create_params + }.to change(Spree::StoreCredit, :count).by(1) + end + + it "redirects to the store credits index page with a success message" do + post solidus_admin.user_store_credits_path(user), params: valid_create_params + expect(response).to redirect_to(solidus_admin.user_store_credits_path(user)) + follow_redirect! + expect(response.body).to include("Store credit was successfully created.") + end + + it "returns a turbo_stream response when requested" do + post solidus_admin.user_store_credits_path(user, format: :turbo_stream), params: valid_create_params + expect(response.media_type).to eq("text/vnd.turbo-stream.html") + expect(response.body).to include('') + end + end + + context "with invalid amount parameters" do + it "does not create a new store credit" do + expect { + post solidus_admin.user_store_credits_path(user), params: invalid_create_amount_params + }.not_to change(Spree::StoreCredit, :count) + end + + it "renders the new template with amount errors" do + post solidus_admin.user_store_credits_path(user), params: invalid_create_amount_params + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to include("must be greater than 0") + end + end + + context "with invalid category parameters" do + it "does not create a new store credit" do + expect { + post solidus_admin.user_store_credits_path(user), params: invalid_create_category_params + }.not_to change(Spree::StoreCredit, :count) + end + + it "renders the new template with category errors" do + post solidus_admin.user_store_credits_path(user), params: invalid_create_category_params + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to include("Store Credit category must be provided") + end + end + end + describe "GET /show" do it "renders the store credit show page with a 200 OK status" do get solidus_admin.user_store_credit_path(user, store_credit) @@ -235,5 +326,14 @@ expect(response.body).to include("Store Credit reason must be provided") end end + + describe "private methods" do + describe "#ensure_store_credit_category" do + it "adds an error when category_id is blank" do + post solidus_admin.user_store_credits_path(user), params: { store_credit: { amount: 100, category_id: nil } } + expect(response.body).to include("Store Credit category must be provided") + end + end + end end end