From ce0dd332439077c9375c68abdd658274fbd01453 Mon Sep 17 00:00:00 2001 From: Regene27 Date: Mon, 29 Jan 2024 11:59:19 +0700 Subject: [PATCH] Close #1037 Add Quantity to Customer and Add Query or Create Variant in Subscription --- .../spree/billing/subscriptions_controller.rb | 32 +++++- .../spree/billing/variants_controller.rb | 10 ++ .../spree_cm_commissioner/subscription.rb | 3 + .../variant_decorator.rb | 2 + .../spree_cm_commissioner/sku_generator.rb | 29 +++++ .../spree_cm_commissioner/variant_checker.rb | 32 ++++++ .../spree_cm_commissioner/variant_creator.rb | 37 +++++++ .../shared/_subscription_tabs.html.erb | 6 +- .../billing/subscriptions/_form.html.erb | 104 +++++++++++++++--- app/views/spree/billing/variants/show.erb | 33 ++++++ config/locales/en.yml | 1 + config/locales/km.yml | 1 + config/routes.rb | 4 + .../sku_generator_spec.rb | 17 +++ .../variant_checker_spec.rb | 33 ++++++ .../variant_creator_spec.rb | 22 ++++ 16 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 app/services/spree_cm_commissioner/sku_generator.rb create mode 100644 app/services/spree_cm_commissioner/variant_checker.rb create mode 100644 app/services/spree_cm_commissioner/variant_creator.rb create mode 100644 app/views/spree/billing/variants/show.erb create mode 100644 spec/services/spree_cm_commissioner/sku_generator_spec.rb create mode 100644 spec/services/spree_cm_commissioner/variant_checker_spec.rb create mode 100644 spec/services/spree_cm_commissioner/variant_creator_spec.rb diff --git a/app/controllers/spree/billing/subscriptions_controller.rb b/app/controllers/spree/billing/subscriptions_controller.rb index 488d304b0..3f1ce688e 100644 --- a/app/controllers/spree/billing/subscriptions_controller.rb +++ b/app/controllers/spree/billing/subscriptions_controller.rb @@ -1,8 +1,22 @@ module Spree module Billing class SubscriptionsController < Spree::Billing::BaseController - before_action :load_customer before_action :load_subscription, if: -> { member_action? } + before_action :load_variant, only: :create + + helper_method :customer + + def create + @subscription = @variant.subscriptions.build(subscription_params.merge(variant_id: @variant.id)) + + if @subscription.save + flash[:success] = flash_message_for(@subscription, :successfully_created) + else + flash[:error] = flash_message_for(@subscription, :not_created) + end + + redirect_to billing_customer_subscriptions_url(customer) + end protected @@ -13,14 +27,14 @@ def collection @collection = @search.result.page(page).per(per_page) end - def load_customer - customer - end - def load_subscription @subscription = @object end + def load_variant + @variant = SpreeCmCommissioner::VariantChecker.new(variant_params, current_vendor).find_or_create_variant + end + def customer @customer ||= SpreeCmCommissioner::Customer.find(params[:customer_id]) end @@ -39,6 +53,14 @@ def object_name def collection_url(options = {}) billing_customer_subscriptions_url(options) end + + def variant_params + params.require(:variant).permit(:product_id, :sku, :price, option_value_ids: []) + end + + def subscription_params + params.require(:spree_cm_commissioner_subscription).permit(:start_date, :customer_id, :status, :variant_id) + end end end end diff --git a/app/controllers/spree/billing/variants_controller.rb b/app/controllers/spree/billing/variants_controller.rb index e240e2ed3..72ffd059e 100644 --- a/app/controllers/spree/billing/variants_controller.rb +++ b/app/controllers/spree/billing/variants_controller.rb @@ -4,6 +4,16 @@ class VariantsController < Spree::Billing::BaseController belongs_to 'spree/product', find_by: :slug before_action :load_data + def new + if request.xhr? + @variant = @product.variants.build + + render :show, layout: false + else + redirect_to new_billing_product_variant_url(@product) + end + end + def collection return @collection if @collection.present? diff --git a/app/models/spree_cm_commissioner/subscription.rb b/app/models/spree_cm_commissioner/subscription.rb index 159bda4cc..dc148ca1c 100644 --- a/app/models/spree_cm_commissioner/subscription.rb +++ b/app/models/spree_cm_commissioner/subscription.rb @@ -6,6 +6,7 @@ class Subscription < SpreeCmCommissioner::Base belongs_to :variant, class_name: 'Spree::Variant' belongs_to :customer, class_name: 'SpreeCmCommissioner::Customer' + belongs_to :product, class_name: 'Spree::Product' has_many :orders, -> { order(:created_at) }, class_name: 'Spree::Order', dependent: :nullify has_many :line_items, through: :orders, class_name: 'Spree::LineItem' @@ -23,6 +24,8 @@ class Subscription < SpreeCmCommissioner::Base after_create :create_order after_commit :update_customer_active_subscriptions_count + accepts_nested_attributes_for :variant + def create_order SpreeCmCommissioner::SubscribedOrderCreator.call(subscription: self) end diff --git a/app/models/spree_cm_commissioner/variant_decorator.rb b/app/models/spree_cm_commissioner/variant_decorator.rb index 9a424fe77..896256a66 100644 --- a/app/models/spree_cm_commissioner/variant_decorator.rb +++ b/app/models/spree_cm_commissioner/variant_decorator.rb @@ -9,6 +9,8 @@ def self.prepended(base) base.validate :validate_option_types base.scope :subscribable, -> { active.joins(:product).where(product: { subscribable: true, status: :active }) } + + base.has_many :subscriptions, class_name: 'SpreeCmCommissioner::Subscription', dependent: :destroy end def selected_option_value_ids diff --git a/app/services/spree_cm_commissioner/sku_generator.rb b/app/services/spree_cm_commissioner/sku_generator.rb new file mode 100644 index 000000000..7c6585185 --- /dev/null +++ b/app/services/spree_cm_commissioner/sku_generator.rb @@ -0,0 +1,29 @@ +module SpreeCmCommissioner + class SkuGenerator + def initialize(product, variant_params) + @product = product + @variant_params = variant_params + end + + def generate_sku + return if @variant_params.nil? || @variant_params.blank? + + option_values = @variant_params[:option_value_ids].map { |id| Spree::OptionValue.find(id) } + sku_parts = [product.name] + + option_values.each do |option_value| + sku_parts << "#{option_value.option_type.name}-#{option_value.name}" + end + + sku_parts << "price-#{@variant_params[:price]}" + + sku_parts.join('-').gsub(' ', '-').downcase + end + + private + + def product + @product ||= Spree::Product.find(@variant_params[:product_id]) + end + end +end diff --git a/app/services/spree_cm_commissioner/variant_checker.rb b/app/services/spree_cm_commissioner/variant_checker.rb new file mode 100644 index 000000000..0fd7f39bc --- /dev/null +++ b/app/services/spree_cm_commissioner/variant_checker.rb @@ -0,0 +1,32 @@ +module SpreeCmCommissioner + class VariantChecker + attr_reader :variant + + def initialize(variant_params, current_vendor) + @product = Spree::Product.find_by(id: variant_params[:product_id]) + @variant_params = variant_params + @current_vendor = current_vendor + end + + def find_or_create_variant + find_variant_by_sku || create_variant + end + + private + + def find_variant_by_sku + @variant = @product.variants.where(sku: variant_sku).first + end + + def create_variant + variant_creator = SpreeCmCommissioner::VariantCreator.new(@product, @variant_params, @current_vendor) + variant_creator.create_variant + @variant = variant_creator.variant + end + + def variant_sku + sku_generator = SpreeCmCommissioner::SkuGenerator.new(@product, @variant_params) + sku_generator.generate_sku + end + end +end diff --git a/app/services/spree_cm_commissioner/variant_creator.rb b/app/services/spree_cm_commissioner/variant_creator.rb new file mode 100644 index 000000000..b517110ae --- /dev/null +++ b/app/services/spree_cm_commissioner/variant_creator.rb @@ -0,0 +1,37 @@ +module SpreeCmCommissioner + class VariantCreator + attr_reader :variant + + def initialize(product, variant_params, current_vendor) + @product = product + @variant_params = variant_params + @current_vendor = current_vendor + end + + def create_variant + @variant = @product.variants.build + generate_sku + set_variant_attributes + @variant.save + @variant + end + + private + + def generate_sku + sku_generator = SpreeCmCommissioner::SkuGenerator.new(@product, @variant_params) + @variant.sku = sku_generator.generate_sku + end + + def set_variant_attributes + @variant.option_value_ids = @variant_params[:option_value_ids] + @variant.price = @variant_params[:price] + stock_item = @variant.stock_items.build(stock_location_id: stock_location, count_on_hand: 1, backorderable: false) + stock_item.save + end + + def stock_location + @stock_location ||= Spree::StockLocation.find_by!(vendor_id: @current_vendor.id).id + end + end +end diff --git a/app/views/spree/billing/shared/_subscription_tabs.html.erb b/app/views/spree/billing/shared/_subscription_tabs.html.erb index a22df5047..30f41a254 100644 --- a/app/views/spree/billing/shared/_subscription_tabs.html.erb +++ b/app/views/spree/billing/shared/_subscription_tabs.html.erb @@ -1,12 +1,12 @@ <% content_for :page_title do %> - <%= page_header_back_button spree.billing_customer_subscriptions_path(@customer) %> + <%= page_header_back_button spree.billing_customer_subscriptions_path(customer) %> <%= @subscription.variant.sku %> <% end %> <% content_for :page_tabs do %> <% end %> @@ -14,7 +14,7 @@ <% end %> diff --git a/app/views/spree/billing/subscriptions/_form.html.erb b/app/views/spree/billing/subscriptions/_form.html.erb index b8d6267f7..9ee37a22f 100644 --- a/app/views/spree/billing/subscriptions/_form.html.erb +++ b/app/views/spree/billing/subscriptions/_form.html.erb @@ -1,22 +1,90 @@
-
- <%= f.hidden_field :customer_id, value: @customer.id %> - <%= f.field_container :variant_id do %> - <%= f.label :variant, Spree.t(:variant) %> - <%= f.collection_select(:variant_id, @customer.subscribable_variants, :id, :display_variant, { include_blank: false }, { class: 'select2' ,disabled: @subscription.persisted?}) %> - <%= f.error_message_on :product_id %> - <% end %> - <%= f.field_container :start_date do %> - <%= f.label :start_date, Spree.t(:start_date) %> - <%= f.date_field :start_date, class: 'form-control', disabled: @subscription.persisted? %> - <%= f.error_message_on :start_date %> - <% end %> - <% if @subscription.id.present? %> - <%= f.field_container :status do %> - <%= f.label :status, Spree.t(:status) %> - <%= f.select :status, SpreeCmCommissioner::Subscription.statuses.keys, {}, class: 'select2' %> - <%= f.error_message_on :status %> +
+ <%= f.hidden_field :customer_id, value: customer.id %> + +
+ <% if @subscription.persisted? %> + <%= f.field_container :product do %> + <%= f.label :product, Spree.t(:product) %> + <%= f.text_field :product, value: f.object.variant.product.name, class: 'form-control', disabled: true %> + <%= f.error_message_on :product %> + <% end %> + <% else %> + <%= f.field_container :product do %> + <%= f.label :product, Spree:t(:product) %> + <%= f.select :product, options_for_select(Spree::Product.joins(:taxons) + .where(spree_taxons: { id: customer.taxon_id }) + .map{ |product| [product.name, product.sku] }), + { include_blank: true }, + { class: "select2-clear" } %> + <%= f.error_message_on :product %> + <% end %> <% end %> - <% end %> +
+ +
+ <% if @subscription.persisted? %> + <%= f.field_container :variant do %> + <%= f.label :variant, Spree.t(:variant) %> + <%= f.text_field :variant, value: f.object.variant.sku, class: 'form-control', disabled: true %> + <%= f.error_message_on :product %> + <% end %> + <% else %> +
+
+ <% end %> +
+ +
+ <%= f.field_container :start_date do %> + <%= f.label :start_date, Spree.t(:start_date) %> + <%= f.date_field :start_date, class: 'form-control', disabled: @subscription.persisted? %> + <%= f.error_message_on :start_date %> + <% end %> +
+ +
+ <% if @subscription.id.present? %> + <%= f.field_container :status do %> + <%= f.label :status, Spree.t(:status) %> + <%= f.select :status, SpreeCmCommissioner::Subscription.statuses.keys, {}, class: 'select2' %> + <%= f.error_message_on :status %> + <% end %> + <% end %> +
+ + \ No newline at end of file diff --git a/app/views/spree/billing/variants/show.erb b/app/views/spree/billing/variants/show.erb new file mode 100644 index 000000000..8bb02de74 --- /dev/null +++ b/app/views/spree/billing/variants/show.erb @@ -0,0 +1,33 @@ + <%= fields_for @variant do |f| %> +
+
+
<%= Spree.t(:variant) %>
+
+ <%= f.hidden_field :product_id, value: @product.id %> + +
+
+
+
+ <%= f.label :price, Spree.t(:price) %> + <%= f.text_field :price, placeholder: I18n.t('spree.billing.subscription_price_placeholder', currency: @variant.currency), value: number_to_currency(@variant.price, unit: ''), class: 'form-control' %> +
+
+
+
+ <% @product.option_types.each do |option_type| %> +
+ <%= label :new_variant, option_type.presentation %> + <% if option_type.name == 'color' %> + <%= f.collection_select 'option_value_ids', option_type.option_values, :id, :name, + { include_blank: true }, { name: 'variant[option_value_ids][]', class: 'select2-clear form-control', id: "option_value_ids-#{option_type.id}" } %> + <% else %> + <%= f.collection_select 'option_value_ids', option_type.option_values, :id, :presentation, + { include_blank: true }, { name: 'variant[option_value_ids][]', class: 'select2-clear form-control', id: "option_value_ids-#{option_type.id}" } %> + <% end %> +
+ <% end %> +
+
+
+ <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 8fc0729cc..1e2c10ed8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -265,6 +265,7 @@ en: overdue: "Overdue" failed: "Failed" unknown: "Unknown" + subscription_price_placeholder: "Subscription Price In %{currency}" upsupported_payment: "Unsupported event" exceeded_available_quantity_on_date: zero: "Rooms are not available on %{date}" diff --git a/config/locales/km.yml b/config/locales/km.yml index c47f338dc..efdc66a7c 100644 --- a/config/locales/km.yml +++ b/config/locales/km.yml @@ -247,6 +247,7 @@ km: overdue: "ហួសថ្ងៃបង់" failed: "បរាជ័យ" unknown: "មិនស្គាល់" + subscription_price_placeholder: "តម្លៃជា​​​ %{currency}" upsupported_payment: "មិនស្គាល់ការបង់ប្រាក់" selected_item_not_available_on_date: "Selected item not available on %{date}" auto_apply: "ដាក់ប្រើដោយស្វ័យប្រវត្តិ" diff --git a/config/routes.rb b/config/routes.rb index 9bee53313..64b892530 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -230,6 +230,10 @@ resources :homepage_section_relatable_options end + namespace :billing do + resources :variants, only: %i[index show] + end + namespace :storefront do resource :cart, controller: :cart, only: %i[show create destroy] do patch :restart_checkout_flow diff --git a/spec/services/spree_cm_commissioner/sku_generator_spec.rb b/spec/services/spree_cm_commissioner/sku_generator_spec.rb new file mode 100644 index 000000000..ca212c56e --- /dev/null +++ b/spec/services/spree_cm_commissioner/sku_generator_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +RSpec.describe SpreeCmCommissioner::SkuGenerator do + let(:product) { create(:product, name: "Waste Collection") } + let(:option_types) { [create(:option_type, name: "Month"), create(:option_type, name: "Due Date"), create(:option_type, name: "Payment Method")] } + let(:option_values) { [create(:option_value, name: "1", option_type: option_types[0]), create(:option_value, name: "1", option_type: option_types[1]), create(:option_value, name: "pre-paid", option_type: option_types[2])] } + let(:variant_params) { { product_id: product.id, option_value_ids: option_values.map(&:id), price: 10.0 } } + let(:sku_generator) { described_class.new(product, variant_params) } + + describe '.generate_sku' do + it 'returns the generated sku' do + generated_sku = sku_generator.generate_sku + + expect(generated_sku).to eq "waste-collection-month-1-due-date-1-payment-method-pre-paid-price-10.0" + end + end +end diff --git a/spec/services/spree_cm_commissioner/variant_checker_spec.rb b/spec/services/spree_cm_commissioner/variant_checker_spec.rb new file mode 100644 index 000000000..bcb51b2f9 --- /dev/null +++ b/spec/services/spree_cm_commissioner/variant_checker_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +RSpec.describe SpreeCmCommissioner::VariantChecker do + let!(:product) { create(:product, name: "Island Waste Collection") } + let!(:option_types) { [create(:option_type, name: "Month"), create(:option_type, name: "Due Date"), create(:option_type, name: "Payment Method")] } + let!(:option_values) { [create(:option_value, name: "1", id: 1, option_type: option_types[0]), create(:option_value, name: "1", id: 2, option_type: option_types[1]), create(:option_value, name: "pre-paid", id: 3, option_type: option_types[2])] } + let!(:variant_params) { { product_id: product.id, option_value_ids: option_values.map(&:id), price: 10.0 } } + let!(:current_vendor) { create(:vendor) } + + describe '#find_or_create_variant' do + context 'when the variant already exists' do + let!(:variant1) { SpreeCmCommissioner::VariantCreator.new(product, variant_params, current_vendor).create_variant } + + it 'returns the existing variant' do + variant_checker = described_class.new(variant_params, current_vendor) + variant = variant_checker.find_or_create_variant + expect(variant).to be_a Spree::Variant + expect(variant).to eq(variant1) + end + end + + context 'when the variant does not exist' do + it 'creates a new variant' do + variant_checker = described_class.new(variant_params, current_vendor) + allow(variant_checker).to receive(:find_variant_by_sku) + expect(variant_checker.send(:find_variant_by_sku)).to eq nil + + variant = variant_checker.find_or_create_variant + expect(variant).to be_a Spree::Variant + end + end + end +end diff --git a/spec/services/spree_cm_commissioner/variant_creator_spec.rb b/spec/services/spree_cm_commissioner/variant_creator_spec.rb new file mode 100644 index 000000000..495ad7ca6 --- /dev/null +++ b/spec/services/spree_cm_commissioner/variant_creator_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +RSpec.describe SpreeCmCommissioner::VariantCreator do + describe '#create_variant' do + let(:product) { create(:product, name: "Waste Collection") } + let(:option_types) { [create(:option_type, name: "Month"), create(:option_type, name: "Due Date"), create(:option_type, name: "Payment Method")] } + let(:option_values) { [create(:option_value, name: "1", id: 1, option_type: option_types[0]), create(:option_value, name: "1", id: 2, option_type: option_types[1]), create(:option_value, name: "pre-paid", id: 3, option_type: option_types[2])] } + let(:variant_params) { { product_id: product.id, option_value_ids: option_values.map(&:id), price: 10.0 } } + let(:sku_generator) { described_class.new(product, variant_params) } + let(:current_vendor) { create(:vendor) } + + it 'creates a variant with the expected attributes' do + variant_creator = SpreeCmCommissioner::VariantCreator.new(product, variant_params, current_vendor) + variant = variant_creator.create_variant + + expect(variant.sku).to eq("waste-collection-month-1-due-date-1-payment-method-pre-paid-price-10.0") + expect(variant.option_value_ids).to eq([1, 2, 3]) + expect(variant.price).to eq(10) + expect(variant.stock_items.first.count_on_hand).to eq(1) + end + end +end