From 29377bbff9704110a0f615e6513379a3fbe8ff50 Mon Sep 17 00:00:00 2001 From: Luis Ramos Date: Tue, 11 Feb 2020 19:34:14 +0000 Subject: [PATCH] Move 5 subscriptions services from app/services to the engines/order_management/app/services --- .rubocop_manual_todo.yml | 7 +- .rubocop_todo.yml | 12 +- .../admin/order_cycles_controller.rb | 4 +- .../subscription_line_items_controller.rb | 2 +- .../admin/subscriptions_controller.rb | 2 +- .../subscription_line_item_serializer.rb | 6 +- app/services/subscription_estimator.rb | 63 --- app/services/subscription_form.rb | 34 -- app/services/subscription_validator.rb | 127 ----- app/services/subscription_variants_service.rb | 39 -- app/services/subscriptions_count.rb | 20 - config/locales/en.yml | 2 +- .../order_management/subscriptions/count.rb | 30 ++ .../subscriptions/estimator.rb | 69 +++ .../order_management/subscriptions/form.rb | 41 ++ .../subscriptions/validator.rb | 132 +++++ .../subscriptions/variants_list.rb | 46 ++ .../subscriptions/count_spec.rb | 40 ++ .../subscriptions/estimator_spec.rb | 135 +++++ .../subscriptions/form_spec.rb | 103 ++++ .../subscriptions/validator_spec.rb | 476 ++++++++++++++++++ .../subscriptions/variants_list_spec.rb | 136 +++++ .../scope_variants_for_search.rb | 2 +- spec/services/subscription_estimator_spec.rb | 129 ----- spec/services/subscription_form_spec.rb | 97 ---- spec/services/subscription_validator_spec.rb | 470 ----------------- .../subscription_variants_service_spec.rb | 130 ----- spec/services/subscriptions_count_spec.rb | 34 -- 28 files changed, 1219 insertions(+), 1169 deletions(-) delete mode 100644 app/services/subscription_estimator.rb delete mode 100644 app/services/subscription_form.rb delete mode 100644 app/services/subscription_validator.rb delete mode 100644 app/services/subscription_variants_service.rb delete mode 100644 app/services/subscriptions_count.rb create mode 100644 engines/order_management/app/services/order_management/subscriptions/count.rb create mode 100644 engines/order_management/app/services/order_management/subscriptions/estimator.rb create mode 100644 engines/order_management/app/services/order_management/subscriptions/form.rb create mode 100644 engines/order_management/app/services/order_management/subscriptions/validator.rb create mode 100644 engines/order_management/app/services/order_management/subscriptions/variants_list.rb create mode 100644 engines/order_management/spec/services/order_management/subscriptions/count_spec.rb create mode 100644 engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb create mode 100644 engines/order_management/spec/services/order_management/subscriptions/form_spec.rb create mode 100644 engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb create mode 100644 engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb delete mode 100644 spec/services/subscription_estimator_spec.rb delete mode 100644 spec/services/subscription_form_spec.rb delete mode 100644 spec/services/subscription_validator_spec.rb delete mode 100644 spec/services/subscription_variants_service_spec.rb delete mode 100644 spec/services/subscriptions_count_spec.rb diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 0ae58c725c..83de12879f 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -98,7 +98,6 @@ Layout/LineLength: - app/services/embedded_page_service.rb - app/services/order_cycle_form.rb - app/services/order_factory.rb - - app/services/subscriptions_count.rb - app/services/variants_stock_levels.rb - engines/web/app/helpers/web/cookies_policy_helper.rb - lib/discourse/single_sign_on.rb @@ -314,10 +313,6 @@ Layout/LineLength: - spec/services/permissions/order_spec.rb - spec/services/product_tag_rules_filterer_spec.rb - spec/services/products_renderer_spec.rb - - spec/services/subscription_estimator_spec.rb - - spec/services/subscription_form_spec.rb - - spec/services/subscription_validator_spec.rb - - spec/services/subscription_variants_service_spec.rb - spec/spec_helper.rb - spec/support/cancan_helper.rb - spec/support/delayed_job_helper.rb @@ -408,7 +403,7 @@ Metrics/AbcSize: - app/services/cart_service.rb - app/services/create_order_cycle.rb - app/services/order_syncer.rb - - app/services/subscription_validator.rb + - engines/order_management/app/services/order_management/subscriptions/validator.rb - lib/active_merchant/billing/gateways/stripe_decorator.rb - lib/active_merchant/billing/gateways/stripe_payment_intents.rb - lib/discourse/single_sign_on.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e498ce594a..984791e600 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -158,7 +158,7 @@ Naming/MethodParameterName: Exclude: - 'app/helpers/spree/admin/base_helper_decorator.rb' - 'app/helpers/spree/base_helper_decorator.rb' - - 'app/services/subscription_validator.rb' + - 'engines/order_management/app/services/order_management/subscriptions/validator.rb' - 'lib/open_food_network/reports/bulk_coop_report.rb' - 'lib/open_food_network/xero_invoices_report.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' @@ -849,11 +849,6 @@ Style/FrozenStringLiteralComment: - 'app/services/reset_order_service.rb' - 'app/services/restart_checkout.rb' - 'app/services/search_orders.rb' - - 'app/services/subscription_estimator.rb' - - 'app/services/subscription_form.rb' - - 'app/services/subscription_validator.rb' - - 'app/services/subscription_variants_service.rb' - - 'app/services/subscriptions_count.rb' - 'app/services/tax_rate_finder.rb' - 'app/services/upload_sanitizer.rb' - 'app/services/variant_deleter.rb' @@ -1350,11 +1345,6 @@ Style/FrozenStringLiteralComment: - 'spec/services/reset_order_service_spec.rb' - 'spec/services/restart_checkout_spec.rb' - 'spec/services/search_orders_spec.rb' - - 'spec/services/subscription_estimator_spec.rb' - - 'spec/services/subscription_form_spec.rb' - - 'spec/services/subscription_validator_spec.rb' - - 'spec/services/subscription_variants_service_spec.rb' - - 'spec/services/subscriptions_count_spec.rb' - 'spec/services/tax_rate_finder_spec.rb' - 'spec/services/upload_sanitizer_spec.rb' - 'spec/services/variants_stock_levels_spec.rb' diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 14d075514a..4a3ad149cb 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -16,7 +16,7 @@ module Admin render_as_json @collection, ams_prefix: params[:ams_prefix], current_user: spree_current_user, - subscriptions_count: SubscriptionsCount.new(@collection) + subscriptions_count: OrderManagement::Subscriptions::Count.new(@collection) end end end @@ -74,7 +74,7 @@ module Admin render_as_json @order_cycles, ams_prefix: 'index', current_user: spree_current_user, - subscriptions_count: SubscriptionsCount.new(@collection) + subscriptions_count: OrderManagement::Subscriptions::Count.new(@collection) else order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? } render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity diff --git a/app/controllers/admin/subscription_line_items_controller.rb b/app/controllers/admin/subscription_line_items_controller.rb index b3649696ac..f8e4945c26 100644 --- a/app/controllers/admin/subscription_line_items_controller.rb +++ b/app/controllers/admin/subscription_line_items_controller.rb @@ -56,7 +56,7 @@ module Admin end def variant_if_eligible(variant_id) - SubscriptionVariantsService.eligible_variants(@shop).find_by_id(variant_id) + OrderManagement::Subscriptions::VariantsList.eligible_variants(@shop).find_by_id(variant_id) end end end diff --git a/app/controllers/admin/subscriptions_controller.rb b/app/controllers/admin/subscriptions_controller.rb index 0b3952d5a6..867c9d2351 100644 --- a/app/controllers/admin/subscriptions_controller.rb +++ b/app/controllers/admin/subscriptions_controller.rb @@ -64,7 +64,7 @@ module Admin private def save_form_and_render(render_issues = true) - form = SubscriptionForm.new(@subscription, params[:subscription]) + form = OrderManagement::Subscriptions::Form.new(@subscription, params[:subscription]) unless form.save render json: { errors: form.json_errors }, status: :unprocessable_entity return diff --git a/app/serializers/api/admin/subscription_line_item_serializer.rb b/app/serializers/api/admin/subscription_line_item_serializer.rb index 34bc00c6c0..f1411e040f 100644 --- a/app/serializers/api/admin/subscription_line_item_serializer.rb +++ b/app/serializers/api/admin/subscription_line_item_serializer.rb @@ -13,9 +13,9 @@ module Api end def in_open_and_upcoming_order_cycles - SubscriptionVariantsService.in_open_and_upcoming_order_cycles?(option_or_assigned_shop, - option_or_assigned_schedule, - object.variant) + OrderManagement::Subscriptions::VariantsList.in_open_and_upcoming_order_cycles?(option_or_assigned_shop, + option_or_assigned_schedule, + object.variant) end private diff --git a/app/services/subscription_estimator.rb b/app/services/subscription_estimator.rb deleted file mode 100644 index 6e4d7b0938..0000000000 --- a/app/services/subscription_estimator.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'open_food_network/scope_variant_to_hub' - -# Responsible for estimating prices and fees for subscriptions -# Used by SubscriptionForm as part of the create/update process -# The values calculated here are intended to be persisted in the db - -class SubscriptionEstimator - def initialize(subscription) - @subscription = subscription - end - - def estimate! - assign_price_estimates - assign_fee_estimates - end - - private - - attr_accessor :subscription - - delegate :subscription_line_items, :shipping_method, :payment_method, :shop, to: :subscription - - def assign_price_estimates - subscription_line_items.each do |item| - item.price_estimate = - price_estimate_for(item.variant, item.price_estimate_was) - end - end - - def price_estimate_for(variant, fallback) - return fallback unless fee_calculator && variant - - scoper.scope(variant) - fees = fee_calculator.indexed_fees_for(variant) - (variant.price + fees).to_d - end - - def fee_calculator - return @fee_calculator unless @fee_calculator.nil? - - next_oc = subscription.schedule.andand.current_or_next_order_cycle - return nil unless shop && next_oc - - @fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc) - end - - def scoper - OpenFoodNetwork::ScopeVariantToHub.new(shop) - end - - def assign_fee_estimates - subscription.shipping_fee_estimate = shipping_fee_estimate - subscription.payment_fee_estimate = payment_fee_estimate - end - - def shipping_fee_estimate - shipping_method.calculator.compute(subscription) - end - - def payment_fee_estimate - payment_method.calculator.compute(subscription) - end -end diff --git a/app/services/subscription_form.rb b/app/services/subscription_form.rb deleted file mode 100644 index fa33b216b2..0000000000 --- a/app/services/subscription_form.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'order_management/subscriptions/proxy_order_syncer' - -class SubscriptionForm - attr_accessor :subscription, :params, :order_update_issues, :validator, :order_syncer, :estimator - - delegate :json_errors, :valid?, to: :validator - delegate :order_update_issues, to: :order_syncer - - def initialize(subscription, params = {}) - @subscription = subscription - @params = params - @estimator = SubscriptionEstimator.new(subscription) - @validator = SubscriptionValidator.new(subscription) - @order_syncer = OrderSyncer.new(subscription) - end - - def save - subscription.assign_attributes(params) - return false unless valid? - - subscription.transaction do - estimator.estimate! - proxy_order_syncer.sync! - order_syncer.sync! - subscription.save! - end - end - - private - - def proxy_order_syncer - OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscription) - end -end diff --git a/app/services/subscription_validator.rb b/app/services/subscription_validator.rb deleted file mode 100644 index 4cce8a3af3..0000000000 --- a/app/services/subscription_validator.rb +++ /dev/null @@ -1,127 +0,0 @@ -# Encapsulation of all of the validation logic required for subscriptions -# Public interface consists of #valid? method provided by ActiveModel::Validations -# and #json_errors which compiles a serializable hash of errors - -class SubscriptionValidator - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Validations - - attr_reader :subscription - - validates :shop, :customer, :schedule, :shipping_method, :payment_method, presence: true - validates :bill_address, :ship_address, :begins_at, presence: true - validate :shipping_method_allowed? - validate :payment_method_allowed? - validate :payment_method_type_allowed? - validate :ends_at_after_begins_at? - validate :customer_allowed? - validate :schedule_allowed? - validate :credit_card_ok? - validate :subscription_line_items_present? - validate :requested_variants_available? - - delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :subscription - delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :subscription - delegate :subscription_line_items, to: :subscription - - def initialize(subscription) - @subscription = subscription - end - - def json_errors - errors.messages.each_with_object({}) do |(k, v), errors| - errors[k] = v.map { |msg| build_msg_from(k, msg) } - end - end - - private - - def shipping_method_allowed? - return unless shipping_method - return if shipping_method.distributors.include?(shop) - - errors.add(:shipping_method, :not_available_to_shop, shop: shop.name) - end - - def payment_method_allowed? - return unless payment_method - return if payment_method.distributors.include?(shop) - - errors.add(:payment_method, :not_available_to_shop, shop: shop.name) - end - - def payment_method_type_allowed? - return unless payment_method - return if Subscription::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type - - errors.add(:payment_method, :invalid_type) - end - - def ends_at_after_begins_at? - # Only validates ends_at if it is present - return if begins_at.blank? || ends_at.blank? - return if ends_at > begins_at - - errors.add(:ends_at, :after_begins_at) - end - - def customer_allowed? - return unless customer - return if customer.enterprise == shop - - errors.add(:customer, :does_not_belong_to_shop, shop: shop.name) - end - - def schedule_allowed? - return unless schedule - return if schedule.coordinators.include?(shop) - - errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name) - end - - def credit_card_ok? - return unless customer && payment_method - return unless stripe_payment_method?(payment_method) - return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges - return if customer.user.andand.default_card.present? - - errors.add(:payment_method, :no_default_card) - end - - def stripe_payment_method?(payment_method) - payment_method.type == "Spree::Gateway::StripeConnect" || - payment_method.type == "Spree::Gateway::StripeSCA" - end - - def subscription_line_items_present? - return if subscription_line_items.reject(&:marked_for_destruction?).any? - - errors.add(:subscription_line_items, :at_least_one_product) - end - - def requested_variants_available? - subscription_line_items.each { |sli| verify_availability_of(sli.variant) } - end - - def verify_availability_of(variant) - return if available_variant_ids.include? variant.id - - name = "#{variant.product.name} - #{variant.full_name}" - errors.add(:subscription_line_items, :not_available, name: name) - end - - def available_variant_ids - return @available_variant_ids if @available_variant_ids.present? - - subscription_variant_ids = subscription_line_items.map(&:variant_id) - @available_variant_ids = SubscriptionVariantsService.eligible_variants(shop) - .where(id: subscription_variant_ids).pluck(:id) - end - - def build_msg_from(k, msg) - return msg[1..-1] if msg.starts_with?("^") - - errors.full_message(k, msg) - end -end diff --git a/app/services/subscription_variants_service.rb b/app/services/subscription_variants_service.rb deleted file mode 100644 index fbdef71e56..0000000000 --- a/app/services/subscription_variants_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -class SubscriptionVariantsService - # Includes the following variants: - # - Variants of permitted producers - # - Variants of hub - # - Variants that are in outgoing exchanges where the hub is receiver - def self.eligible_variants(distributor) - variant_conditions = ["spree_products.supplier_id IN (?)", permitted_producer_ids(distributor)] - exchange_variant_ids = outgoing_exchange_variant_ids(distributor) - if exchange_variant_ids.present? - variant_conditions[0] << " OR spree_variants.id IN (?)" - variant_conditions << exchange_variant_ids - end - - Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions) - end - - def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant) - scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules }) - .where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor }) - .merge(OrderCycle.not_closed) - scope = scope.where(schedules: { id: schedule }) - scope.any? - end - - def self.permitted_producer_ids(distributor) - other_permitted_producer_ids = EnterpriseRelationship.joins(:parent) - .permitting(distributor.id).with_permission(:add_to_order_cycle) - .merge(Enterprise.is_primary_producer) - .pluck(:parent_id) - - other_permitted_producer_ids | [distributor.id] - end - - def self.outgoing_exchange_variant_ids(distributor) - ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange) - .where(exchanges: { incoming: false, receiver_id: distributor.id }) - .pluck(:variant_id) - end -end diff --git a/app/services/subscriptions_count.rb b/app/services/subscriptions_count.rb deleted file mode 100644 index ee1126c0d2..0000000000 --- a/app/services/subscriptions_count.rb +++ /dev/null @@ -1,20 +0,0 @@ -class SubscriptionsCount - def initialize(order_cycles) - @order_cycles = order_cycles - end - - def for(order_cycle_id) - active[order_cycle_id] || 0 - end - - private - - attr_accessor :order_cycles - - def active - return @active unless @active.nil? - return @active = [] if order_cycles.blank? - - @active ||= ProxyOrder.not_canceled.group(:order_cycle_id).where(order_cycle_id: order_cycles).count - end -end diff --git a/config/locales/en.yml b/config/locales/en.yml index 7b9c07620f..8f2e0b53b1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -74,7 +74,7 @@ en: messages: inclusion: "is not included in the list" models: - subscription_validator: + order_management/subscriptions/validator: attributes: subscription_line_items: at_least_one_product: "^Please add at least one product" diff --git a/engines/order_management/app/services/order_management/subscriptions/count.rb b/engines/order_management/app/services/order_management/subscriptions/count.rb new file mode 100644 index 0000000000..80f066d209 --- /dev/null +++ b/engines/order_management/app/services/order_management/subscriptions/count.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module OrderManagement + module Subscriptions + class Count + def initialize(order_cycles) + @order_cycles = order_cycles + end + + def for(order_cycle_id) + active[order_cycle_id] || 0 + end + + private + + attr_accessor :order_cycles + + def active + return @active unless @active.nil? + return @active = [] if order_cycles.blank? + + @active ||= ProxyOrder. + not_canceled. + group(:order_cycle_id). + where(order_cycle_id: order_cycles). + count + end + end + end +end diff --git a/engines/order_management/app/services/order_management/subscriptions/estimator.rb b/engines/order_management/app/services/order_management/subscriptions/estimator.rb new file mode 100644 index 0000000000..e358e031bf --- /dev/null +++ b/engines/order_management/app/services/order_management/subscriptions/estimator.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'open_food_network/scope_variant_to_hub' + +# Responsible for estimating prices and fees for subscriptions +# Used by Form as part of the create/update process +# The values calculated here are intended to be persisted in the db + +module OrderManagement + module Subscriptions + class Estimator + def initialize(subscription) + @subscription = subscription + end + + def estimate! + assign_price_estimates + assign_fee_estimates + end + + private + + attr_accessor :subscription + + delegate :subscription_line_items, :shipping_method, :payment_method, :shop, to: :subscription + + def assign_price_estimates + subscription_line_items.each do |item| + item.price_estimate = + price_estimate_for(item.variant, item.price_estimate_was) + end + end + + def price_estimate_for(variant, fallback) + return fallback unless fee_calculator && variant + + scoper.scope(variant) + fees = fee_calculator.indexed_fees_for(variant) + (variant.price + fees).to_d + end + + def fee_calculator + return @fee_calculator unless @fee_calculator.nil? + + next_oc = subscription.schedule.andand.current_or_next_order_cycle + return nil unless shop && next_oc + + @fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc) + end + + def scoper + OpenFoodNetwork::ScopeVariantToHub.new(shop) + end + + def assign_fee_estimates + subscription.shipping_fee_estimate = shipping_fee_estimate + subscription.payment_fee_estimate = payment_fee_estimate + end + + def shipping_fee_estimate + shipping_method.calculator.compute(subscription) + end + + def payment_fee_estimate + payment_method.calculator.compute(subscription) + end + end + end +end diff --git a/engines/order_management/app/services/order_management/subscriptions/form.rb b/engines/order_management/app/services/order_management/subscriptions/form.rb new file mode 100644 index 0000000000..02994e3b50 --- /dev/null +++ b/engines/order_management/app/services/order_management/subscriptions/form.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'order_management/subscriptions/proxy_order_syncer' + +module OrderManagement + module Subscriptions + class Form + attr_accessor :subscription, :params, :order_update_issues, + :validator, :order_syncer, :estimator + + delegate :json_errors, :valid?, to: :validator + delegate :order_update_issues, to: :order_syncer + + def initialize(subscription, params = {}) + @subscription = subscription + @params = params + @estimator = OrderManagement::Subscriptions::Estimator.new(subscription) + @validator = OrderManagement::Subscriptions::Validator.new(subscription) + @order_syncer = OrderSyncer.new(subscription) + end + + def save + subscription.assign_attributes(params) + return false unless valid? + + subscription.transaction do + estimator.estimate! + proxy_order_syncer.sync! + order_syncer.sync! + subscription.save! + end + end + + private + + def proxy_order_syncer + OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscription) + end + end + end +end diff --git a/engines/order_management/app/services/order_management/subscriptions/validator.rb b/engines/order_management/app/services/order_management/subscriptions/validator.rb new file mode 100644 index 0000000000..a8bc6b3086 --- /dev/null +++ b/engines/order_management/app/services/order_management/subscriptions/validator.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +# Encapsulation of all of the validation logic required for subscriptions +# Public interface consists of #valid? method provided by ActiveModel::Validations +# and #json_errors which compiles a serializable hash of errors +module OrderManagement + module Subscriptions + class Validator + include ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + + attr_reader :subscription + + validates :shop, :customer, :schedule, :shipping_method, :payment_method, presence: true + validates :bill_address, :ship_address, :begins_at, presence: true + validate :shipping_method_allowed? + validate :payment_method_allowed? + validate :payment_method_type_allowed? + validate :ends_at_after_begins_at? + validate :customer_allowed? + validate :schedule_allowed? + validate :credit_card_ok? + validate :subscription_line_items_present? + validate :requested_variants_available? + + delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :subscription + delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :subscription + delegate :subscription_line_items, to: :subscription + + def initialize(subscription) + @subscription = subscription + end + + def json_errors + errors.messages.each_with_object({}) do |(key, value), errors| + errors[key] = value.map { |msg| build_msg_from(key, msg) } + end + end + + private + + def shipping_method_allowed? + return unless shipping_method + return if shipping_method.distributors.include?(shop) + + errors.add(:shipping_method, :not_available_to_shop, shop: shop.name) + end + + def payment_method_allowed? + return unless payment_method + return if payment_method.distributors.include?(shop) + + errors.add(:payment_method, :not_available_to_shop, shop: shop.name) + end + + def payment_method_type_allowed? + return unless payment_method + return if Subscription::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type + + errors.add(:payment_method, :invalid_type) + end + + def ends_at_after_begins_at? + # Only validates ends_at if it is present + return if begins_at.blank? || ends_at.blank? + return if ends_at > begins_at + + errors.add(:ends_at, :after_begins_at) + end + + def customer_allowed? + return unless customer + return if customer.enterprise == shop + + errors.add(:customer, :does_not_belong_to_shop, shop: shop.name) + end + + def schedule_allowed? + return unless schedule + return if schedule.coordinators.include?(shop) + + errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name) + end + + def credit_card_ok? + return unless customer && payment_method + return unless stripe_payment_method?(payment_method) + return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges + return if customer.user.andand.default_card.present? + + errors.add(:payment_method, :no_default_card) + end + + def stripe_payment_method?(payment_method) + payment_method.type == "Spree::Gateway::StripeConnect" || + payment_method.type == "Spree::Gateway::StripeSCA" + end + + def subscription_line_items_present? + return if subscription_line_items.reject(&:marked_for_destruction?).any? + + errors.add(:subscription_line_items, :at_least_one_product) + end + + def requested_variants_available? + subscription_line_items.each { |sli| verify_availability_of(sli.variant) } + end + + def verify_availability_of(variant) + return if available_variant_ids.include? variant.id + + name = "#{variant.product.name} - #{variant.full_name}" + errors.add(:subscription_line_items, :not_available, name: name) + end + + def available_variant_ids + return @available_variant_ids if @available_variant_ids.present? + + subscription_variant_ids = subscription_line_items.map(&:variant_id) + @available_variant_ids = OrderManagement::Subscriptions::VariantsList.eligible_variants(shop) + .where(id: subscription_variant_ids).pluck(:id) + end + + def build_msg_from(key, msg) + return msg[1..-1] if msg.starts_with?("^") + + errors.full_message(key, msg) + end + end + end +end diff --git a/engines/order_management/app/services/order_management/subscriptions/variants_list.rb b/engines/order_management/app/services/order_management/subscriptions/variants_list.rb new file mode 100644 index 0000000000..5c5a3fddb3 --- /dev/null +++ b/engines/order_management/app/services/order_management/subscriptions/variants_list.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: false + +module OrderManagement + module Subscriptions + class VariantsList + # Includes the following variants: + # - Variants of permitted producers + # - Variants of hub + # - Variants that are in outgoing exchanges where the hub is receiver + def self.eligible_variants(distributor) + variant_conditions = ["spree_products.supplier_id IN (?)", + permitted_producer_ids(distributor)] + exchange_variant_ids = outgoing_exchange_variant_ids(distributor) + if exchange_variant_ids.present? + variant_conditions[0] << " OR spree_variants.id IN (?)" + variant_conditions << exchange_variant_ids + end + + Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions) + end + + def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant) + scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules }) + .where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor }) + .merge(OrderCycle.not_closed) + scope = scope.where(schedules: { id: schedule }) + scope.any? + end + + def self.permitted_producer_ids(distributor) + other_permitted_producer_ids = EnterpriseRelationship.joins(:parent) + .permitting(distributor.id).with_permission(:add_to_order_cycle) + .merge(Enterprise.is_primary_producer) + .pluck(:parent_id) + + other_permitted_producer_ids | [distributor.id] + end + + def self.outgoing_exchange_variant_ids(distributor) + ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange) + .where(exchanges: { incoming: false, receiver_id: distributor.id }) + .pluck(:variant_id) + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/subscriptions/count_spec.rb b/engines/order_management/spec/services/order_management/subscriptions/count_spec.rb new file mode 100644 index 0000000000..14b5a8c041 --- /dev/null +++ b/engines/order_management/spec/services/order_management/subscriptions/count_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module OrderManagement + module Subscriptions + describe Count do + let(:oc1) { create(:simple_order_cycle) } + let(:oc2) { create(:simple_order_cycle) } + let(:subscriptions_count) { Count.new(order_cycles) } + + describe "#for" do + context "when the collection has not been set" do + let(:order_cycles) { nil } + it "returns 0" do + expect(subscriptions_count.for(oc1.id)).to eq 0 + end + end + + context "when the collection has been set" do + let(:order_cycles) { OrderCycle.where(id: [oc1]) } + let!(:po1) { create(:proxy_order, order_cycle: oc1) } + let!(:po2) { create(:proxy_order, order_cycle: oc1) } + let!(:po3) { create(:proxy_order, order_cycle: oc2) } + + context "but the requested id is not present in the list of order cycles provided" do + it "returns 0" do + # Note that po3 applies to oc2, but oc2 in not in the collection + expect(subscriptions_count.for(oc2.id)).to eq 0 + end + end + + context "and the requested id is present in the list of order cycles provided" do + it "returns a count of active proxy orders associated with the requested order cycle" do + expect(subscriptions_count.for(oc1.id)).to eq 2 + end + end + end + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb b/engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb new file mode 100644 index 0000000000..78020f6578 --- /dev/null +++ b/engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module OrderManagement + module Subscriptions + describe Estimator do + describe "estimating prices for subscription line items" do + let!(:subscription) { create(:subscription, with_items: true) } + let!(:sli1) { subscription.subscription_line_items.first } + let!(:sli2) { subscription.subscription_line_items.second } + let!(:sli3) { subscription.subscription_line_items.third } + let(:estimator) { Estimator.new(subscription) } + + before do + sli1.update_attributes(price_estimate: 4.0) + sli2.update_attributes(price_estimate: 5.0) + sli3.update_attributes(price_estimate: 6.0) + sli1.variant.update_attributes(price: 1.0) + sli2.variant.update_attributes(price: 2.0) + sli3.variant.update_attributes(price: 3.0) + + # Simulating assignment of attrs from params + sli1.assign_attributes(price_estimate: 7.0) + sli2.assign_attributes(price_estimate: 8.0) + sli3.assign_attributes(price_estimate: 9.0) + end + + context "when a insufficient information exists to calculate price estimates" do + before do + # This might be because a shop has not been assigned yet, or no + # current or future order cycles exist for the schedule + allow(estimator).to receive(:fee_calculator) { nil } + end + + it "resets the price estimates for all items" do + estimator.estimate! + expect(sli1.price_estimate).to eq 4.0 + expect(sli2.price_estimate).to eq 5.0 + expect(sli3.price_estimate).to eq 6.0 + end + end + + context "when sufficient information to calculate price estimates exists" do + let(:fee_calculator) { instance_double(OpenFoodNetwork::EnterpriseFeeCalculator) } + + before do + allow(estimator).to receive(:fee_calculator) { fee_calculator } + allow(fee_calculator).to receive(:indexed_fees_for).with(sli1.variant) { 1.0 } + allow(fee_calculator).to receive(:indexed_fees_for).with(sli2.variant) { 0.0 } + allow(fee_calculator).to receive(:indexed_fees_for).with(sli3.variant) { 3.0 } + end + + context "when no variant overrides apply" do + it "recalculates price_estimates based on variant prices and associated fees" do + estimator.estimate! + expect(sli1.price_estimate).to eq 2.0 + expect(sli2.price_estimate).to eq 2.0 + expect(sli3.price_estimate).to eq 6.0 + end + end + + context "when variant overrides apply" do + let!(:override1) { create(:variant_override, hub: subscription.shop, variant: sli1.variant, price: 1.2) } + let!(:override2) { create(:variant_override, hub: subscription.shop, variant: sli2.variant, price: 2.3) } + + it "recalculates price_estimates based on override prices and associated fees" do + estimator.estimate! + expect(sli1.price_estimate).to eq 2.2 + expect(sli2.price_estimate).to eq 2.3 + expect(sli3.price_estimate).to eq 6.0 + end + end + end + end + + describe "updating estimates for shipping and payment fees" do + let(:subscription) { create(:subscription, with_items: true, payment_method: payment_method, shipping_method: shipping_method) } + let!(:sli1) { subscription.subscription_line_items.first } + let!(:sli2) { subscription.subscription_line_items.second } + let!(:sli3) { subscription.subscription_line_items.third } + let(:estimator) { OrderManagement::Subscriptions::Estimator.new(subscription) } + + before do + allow(estimator).to receive(:assign_price_estimates) + sli1.update_attributes(price_estimate: 4.0) + sli2.update_attributes(price_estimate: 5.0) + sli3.update_attributes(price_estimate: 6.0) + end + + context "using flat rate calculators" do + let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 12.34)) } + let(:payment_method) { create(:payment_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 9.12)) } + + it "calculates fees based on the rates provided" do + estimator.estimate! + expect(subscription.shipping_fee_estimate.to_f).to eq 12.34 + expect(subscription.payment_fee_estimate.to_f).to eq 9.12 + end + end + + context "using flat percent item total calculators" do + let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } + let(:payment_method) { create(:payment_method, calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) } + + it "calculates fees based on the estimated item total and percentage provided" do + estimator.estimate! + expect(subscription.shipping_fee_estimate.to_f).to eq 1.5 + expect(subscription.payment_fee_estimate.to_f).to eq 3.0 + end + end + + context "using flat percent per item calculators" do + let(:shipping_method) { create(:shipping_method, calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 5)) } + let(:payment_method) { create(:payment_method, calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10)) } + + it "calculates fees based on the estimated item prices and percentage provided" do + estimator.estimate! + expect(subscription.shipping_fee_estimate.to_f).to eq 0.75 + expect(subscription.payment_fee_estimate.to_f).to eq 1.5 + end + end + + context "using per item calculators" do + let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::PerItem.new(preferred_amount: 1.2)) } + let(:payment_method) { create(:payment_method, calculator: Spree::Calculator::PerItem.new(preferred_amount: 0.3)) } + + it "calculates fees based on the number of items and rate provided" do + estimator.estimate! + expect(subscription.shipping_fee_estimate.to_f).to eq 3.6 + expect(subscription.payment_fee_estimate.to_f).to eq 0.9 + end + end + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/subscriptions/form_spec.rb b/engines/order_management/spec/services/order_management/subscriptions/form_spec.rb new file mode 100644 index 0000000000..f9c0798671 --- /dev/null +++ b/engines/order_management/spec/services/order_management/subscriptions/form_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module OrderManagement + module Subscriptions + describe Form do + describe "creating a new subscription" do + let!(:shop) { create(:distributor_enterprise) } + let!(:customer) { create(:customer, enterprise: shop) } + let!(:product1) { create(:product, supplier: shop) } + let!(:product2) { create(:product, supplier: shop) } + let!(:product3) { create(:product, supplier: shop) } + let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) } + let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) } + let!(:variant3) { create(:variant, product: product2, unit_value: '1000', price: 2.50, option_values: [], on_hand: 1) } + let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) } + let!(:order_cycle1) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 9.days.ago, orders_close_at: 2.days.ago) } + let!(:order_cycle2) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.ago, orders_close_at: 5.days.from_now) } + let!(:order_cycle3) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 5.days.from_now, orders_close_at: 12.days.from_now) } + let!(:order_cycle4) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 12.days.from_now, orders_close_at: 19.days.from_now) } + let!(:outgoing_exchange1) { order_cycle1.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2, variant3], enterprise_fees: [enterprise_fee]) } + let!(:outgoing_exchange2) { order_cycle2.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2, variant3], enterprise_fees: [enterprise_fee]) } + let!(:outgoing_exchange3) { order_cycle3.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant3], enterprise_fees: []) } + let!(:outgoing_exchange4) { order_cycle4.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2, variant3], enterprise_fees: [enterprise_fee]) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle1, order_cycle2, order_cycle3, order_cycle4]) } + let!(:payment_method) { create(:payment_method, distributors: [shop]) } + let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } + let!(:address) { create(:address) } + let(:subscription) { Subscription.new } + + let!(:params) { + { + shop_id: shop.id, + customer_id: customer.id, + schedule_id: schedule.id, + bill_address_attributes: address.clone.attributes, + ship_address_attributes: address.clone.attributes, + payment_method_id: payment_method.id, + shipping_method_id: shipping_method.id, + begins_at: 4.days.ago, + ends_at: 14.days.from_now, + subscription_line_items_attributes: [ + { variant_id: variant1.id, quantity: 1, price_estimate: 7.0 }, + { variant_id: variant2.id, quantity: 2, price_estimate: 8.0 }, + { variant_id: variant3.id, quantity: 3, price_estimate: 9.0 } + ] + } + } + + let(:form) { OrderManagement::Subscriptions::Form.new(subscription, params) } + + it "creates orders for each order cycle in the schedule" do + expect(form.save).to be true + + expect(subscription.proxy_orders.count).to be 2 + expect(subscription.subscription_line_items.count).to be 3 + expect(subscription.subscription_line_items[0].price_estimate).to eq 13.75 + expect(subscription.subscription_line_items[1].price_estimate).to eq 7.75 + expect(subscription.subscription_line_items[2].price_estimate).to eq 4.25 + + # This order cycle has already closed, so no order is initialized + proxy_order1 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle1.id) + expect(proxy_order1).to be nil + + # Currently open order cycle, closing after begins_at and before ends_at + proxy_order2 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle2.id) + expect(proxy_order2).to be_a ProxyOrder + order2 = proxy_order2.initialise_order! + expect(order2.line_items.count).to eq 3 + expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3 + expect(order2.shipments.count).to eq 1 + expect(order2.shipments.first.shipping_method).to eq shipping_method + expect(order2.payments.count).to eq 1 + expect(order2.payments.first.payment_method).to eq payment_method + expect(order2.payments.first.state).to eq 'checkout' + expect(order2.total).to eq 42 + expect(order2.completed?).to be false + + # Future order cycle, closing after begins_at and before ends_at + # Adds line items for variants that aren't yet available from the order cycle + proxy_order3 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle3.id) + expect(proxy_order3).to be_a ProxyOrder + order3 = proxy_order3.initialise_order! + expect(order3).to be_a Spree::Order + expect(order3.line_items.count).to eq 3 + expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3 + expect(order3.shipments.count).to eq 1 + expect(order3.shipments.first.shipping_method).to eq shipping_method + expect(order3.payments.count).to eq 1 + expect(order3.payments.first.payment_method).to eq payment_method + expect(order3.payments.first.state).to eq 'checkout' + expect(order3.total).to eq 31.50 + expect(order3.completed?).to be false + + # Future order cycle closing after ends_at + proxy_order4 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle4.id) + expect(proxy_order4).to be nil + end + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb b/engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb new file mode 100644 index 0000000000..a072262b47 --- /dev/null +++ b/engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb @@ -0,0 +1,476 @@ +# frozen_string_literal: true + +require "spec_helper" + +module OrderManagement + module Subscriptions + describe Validator do + let(:owner) { create(:user) } + let(:shop) { create(:enterprise, name: "Shop", owner: owner) } + + describe "delegation" do + let(:subscription) { create(:subscription, shop: shop) } + let(:validator) { Validator.new(subscription) } + + it "delegates to subscription" do + expect(validator.shop).to eq subscription.shop + expect(validator.customer).to eq subscription.customer + expect(validator.schedule).to eq subscription.schedule + expect(validator.shipping_method).to eq subscription.shipping_method + expect(validator.payment_method).to eq subscription.payment_method + expect(validator.bill_address).to eq subscription.bill_address + expect(validator.ship_address).to eq subscription.ship_address + expect(validator.begins_at).to eq subscription.begins_at + expect(validator.ends_at).to eq subscription.ends_at + end + end + + describe "validations" do + let(:subscription_stubs) do + { + shop: shop, + customer: true, + schedule: true, + shipping_method: true, + payment_method: true, + bill_address: true, + ship_address: true, + begins_at: true, + ends_at: true, + } + end + + let(:validation_stubs) do + { + shipping_method_allowed?: true, + payment_method_allowed?: true, + payment_method_type_allowed?: true, + ends_at_after_begins_at?: true, + customer_allowed?: true, + schedule_allowed?: true, + credit_card_ok?: true, + subscription_line_items_present?: true, + requested_variants_available?: true + } + end + + let(:subscription) { instance_double(Subscription, subscription_stubs) } + let(:validator) { OrderManagement::Subscriptions::Validator.new(subscription) } + + def stub_validations(validator, methods) + methods.each do |name, value| + allow(validator).to receive(name) { value } + end + end + + describe "shipping method validation" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:shipping_method)) } + before { stub_validations(validator, validation_stubs.except(:shipping_method_allowed?)) } + + context "when no shipping method is present" do + before { expect(subscription).to receive(:shipping_method).at_least(:once) { nil } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:shipping_method]).to_not be_empty + end + end + + context "when a shipping method is present" do + let(:shipping_method) { instance_double(Spree::ShippingMethod, distributors: [shop]) } + before { expect(subscription).to receive(:shipping_method).at_least(:once) { shipping_method } } + + context "and the shipping method is not associated with the shop" do + before { allow(shipping_method).to receive(:distributors) { [double(:enterprise)] } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:shipping_method]).to_not be_empty + end + end + + context "and the shipping method is associated with the shop" do + before { allow(shipping_method).to receive(:distributors) { [shop] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:shipping_method]).to be_empty + end + end + end + end + + describe "payment method validation" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:payment_method)) } + before { stub_validations(validator, validation_stubs.except(:payment_method_allowed?)) } + + context "when no payment method is present" do + before { expect(subscription).to receive(:payment_method).at_least(:once) { nil } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "when a payment method is present" do + let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) } + before { expect(subscription).to receive(:payment_method).at_least(:once) { payment_method } } + + context "and the payment method is not associated with the shop" do + before { allow(payment_method).to receive(:distributors) { [double(:enterprise)] } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "and the payment method is associated with the shop" do + before { allow(payment_method).to receive(:distributors) { [shop] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:payment_method]).to be_empty + end + end + end + end + + describe "payment method type validation" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:payment_method)) } + before { stub_validations(validator, validation_stubs.except(:payment_method_type_allowed?)) } + + context "when a payment method is present" do + let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) } + before { expect(subscription).to receive(:payment_method).at_least(:once) { payment_method } } + + context "and the payment method type is not in the approved list" do + before { allow(payment_method).to receive(:type) { "Blah" } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "and the payment method is in the approved list" do + let(:approved_type) { Subscription::ALLOWED_PAYMENT_METHOD_TYPES.first } + before { allow(payment_method).to receive(:type) { approved_type } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:payment_method]).to be_empty + end + end + end + end + + describe "dates" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:begins_at, :ends_at)) } + before { stub_validations(validator, validation_stubs.except(:ends_at_after_begins_at?)) } + before { expect(subscription).to receive(:begins_at).at_least(:once) { begins_at } } + + context "when no begins_at is present" do + let(:begins_at) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:begins_at]).to_not be_empty + end + end + + context "when a start date is present" do + let(:begins_at) { Time.zone.today } + before { expect(subscription).to receive(:ends_at).at_least(:once) { ends_at } } + + context "when no ends_at is present" do + let(:ends_at) { nil } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:ends_at]).to be_empty + end + end + + context "when ends_at is equal to begins_at" do + let(:ends_at) { Time.zone.today } + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:ends_at]).to_not be_empty + end + end + + context "when ends_at is before begins_at" do + let(:ends_at) { Time.zone.today - 1.day } + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:ends_at]).to_not be_empty + end + end + + context "when ends_at is after begins_at" do + let(:ends_at) { Time.zone.today + 1.day } + it "adds an error and returns false" do + expect(validator.valid?).to be true + expect(validator.errors[:ends_at]).to be_empty + end + end + end + end + + describe "addresses" do + before { stub_validations(validator, validation_stubs) } + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:bill_address, :ship_address)) } + before { expect(subscription).to receive(:bill_address).at_least(:once) { bill_address } } + before { expect(subscription).to receive(:ship_address).at_least(:once) { ship_address } } + + context "when bill_address and ship_address are not present" do + let(:bill_address) { nil } + let(:ship_address) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:bill_address]).to_not be_empty + expect(validator.errors[:ship_address]).to_not be_empty + end + end + + context "when bill_address and ship_address are present" do + let(:bill_address) { instance_double(Spree::Address) } + let(:ship_address) { instance_double(Spree::Address) } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:bill_address]).to be_empty + expect(validator.errors[:ship_address]).to be_empty + end + end + end + + describe "customer" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:customer)) } + before { stub_validations(validator, validation_stubs.except(:customer_allowed?)) } + before { expect(subscription).to receive(:customer).at_least(:once) { customer } } + + context "when no customer is present" do + let(:customer) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:customer]).to_not be_empty + end + end + + context "when a customer is present" do + let(:customer) { instance_double(Customer) } + + context "and the customer is not associated with the shop" do + before { allow(customer).to receive(:enterprise) { double(:enterprise) } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:customer]).to_not be_empty + end + end + + context "and the customer is associated with the shop" do + before { allow(customer).to receive(:enterprise) { shop } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:customer]).to be_empty + end + end + end + end + + describe "schedule" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:schedule)) } + before { stub_validations(validator, validation_stubs.except(:schedule_allowed?)) } + before { expect(subscription).to receive(:schedule).at_least(:once) { schedule } } + + context "when no schedule is present" do + let(:schedule) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:schedule]).to_not be_empty + end + end + + context "when a schedule is present" do + let(:schedule) { instance_double(Schedule) } + + context "and the schedule is not associated with the shop" do + before { allow(schedule).to receive(:coordinators) { [double(:enterprise)] } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:schedule]).to_not be_empty + end + end + + context "and the schedule is associated with the shop" do + before { allow(schedule).to receive(:coordinators) { [shop] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:schedule]).to be_empty + end + end + end + end + + describe "credit card" do + let(:subscription) { instance_double(Subscription, subscription_stubs.except(:payment_method)) } + before { stub_validations(validator, validation_stubs.except(:credit_card_ok?)) } + before { expect(subscription).to receive(:payment_method).at_least(:once) { payment_method } } + + context "when using a Check payment method" do + let(:payment_method) { instance_double(Spree::PaymentMethod, type: "Spree::PaymentMethod::Check") } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:subscription_line_items]).to be_empty + end + end + + context "when using the StripeConnect payment gateway" do + let(:payment_method) { instance_double(Spree::PaymentMethod, type: "Spree::Gateway::StripeConnect") } + before { expect(subscription).to receive(:customer).at_least(:once) { customer } } + + context "when the customer does not allow charges" do + let(:customer) { instance_double(Customer, allow_charges: false) } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "when the customer allows charges" do + let(:customer) { instance_double(Customer, allow_charges: true) } + + context "and the customer is not associated with a user" do + before { allow(customer).to receive(:user) { nil } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "and the customer is associated with a user" do + before { expect(customer).to receive(:user).once { user } } + + context "and the user has no default card set" do + let(:user) { instance_double(Spree::User, default_card: nil) } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "and the user has a default card set" do + let(:user) { instance_double(Spree::User, default_card: 'some card') } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:payment_method]).to be_empty + end + end + end + end + end + end + + describe "subscription line items" do + let(:subscription) { instance_double(Subscription, subscription_stubs) } + before { stub_validations(validator, validation_stubs.except(:subscription_line_items_present?)) } + before { expect(subscription).to receive(:subscription_line_items).at_least(:once) { subscription_line_items } } + + context "when no subscription line items are present" do + let(:subscription_line_items) { [] } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:subscription_line_items]).to_not be_empty + end + end + + context "when subscription line items are present but they are all marked for destruction" do + let(:subscription_line_item1) { instance_double(SubscriptionLineItem, marked_for_destruction?: true) } + let(:subscription_line_items) { [subscription_line_item1] } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:subscription_line_items]).to_not be_empty + end + end + + context "when subscription line items are present and some and not marked for destruction" do + let(:subscription_line_item1) { instance_double(SubscriptionLineItem, marked_for_destruction?: true) } + let(:subscription_line_item2) { instance_double(SubscriptionLineItem, marked_for_destruction?: false) } + let(:subscription_line_items) { [subscription_line_item1, subscription_line_item2] } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:subscription_line_items]).to be_empty + end + end + end + + describe "variant availability" do + let(:subscription) { instance_double(Subscription, subscription_stubs) } + before { stub_validations(validator, validation_stubs.except(:requested_variants_available?)) } + before { expect(subscription).to receive(:subscription_line_items).at_least(:once) { subscription_line_items } } + + context "when no subscription line items are present" do + let(:subscription_line_items) { [] } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:subscription_line_items]).to be_empty + end + end + + context "when subscription line items are present" do + let(:variant1) { instance_double(Spree::Variant, id: 1) } + let(:variant2) { instance_double(Spree::Variant, id: 2) } + let(:subscription_line_item1) { instance_double(SubscriptionLineItem, variant: variant1) } + let(:subscription_line_item2) { instance_double(SubscriptionLineItem, variant: variant2) } + let(:subscription_line_items) { [subscription_line_item1] } + + context "but some variants are unavailable" do + let(:product) { instance_double(Spree::Product, name: "some_name") } + + before do + allow(validator).to receive(:available_variant_ids) { [variant2.id] } + allow(variant1).to receive(:product) { product } + allow(variant1).to receive(:full_name) { "some name" } + end + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:subscription_line_items]).to_not be_empty + end + end + + context "and all requested variants are available" do + before do + allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } + end + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:subscription_line_items]).to be_empty + end + end + end + end + end + end + end +end diff --git a/engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb b/engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb new file mode 100644 index 0000000000..8dc97b128e --- /dev/null +++ b/engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "spec_helper" + +module OrderManagement + module Subscriptions + describe VariantsList do + describe "variant eligibility for subscription" do + let!(:shop) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } + let!(:product) { create(:product, supplier: producer) } + let!(:variant) { product.variants.first } + + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) } + let!(:subscription_line_item) do + create(:subscription_line_item, subscription: subscription, variant: variant) + end + + let(:current_order_cycle) do + create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago, + orders_close_at: 1.week.from_now) + end + + let(:future_order_cycle) do + create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now, + orders_close_at: 2.weeks.from_now) + end + + let(:past_order_cycle) do + create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago, + orders_close_at: 1.week.ago) + end + + let!(:order_cycle) { current_order_cycle } + + context "if the shop is the supplier for the product" do + let!(:producer) { shop } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the supplier is permitted for the shop" do + let!(:enterprise_relationship) { create(:enterprise_relationship, child: shop, parent: product.supplier, permissions_list: [:add_to_order_cycle]) } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the variant is involved in an exchange" do + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + + context "if it is an incoming exchange where the shop is the receiver" do + let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) } + + it "is not eligible" do + expect(described_class.eligible_variants(shop)).to_not include(variant) + end + end + + context "if it is an outgoing exchange where the shop is the receiver" do + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) } + + context "if the order cycle is currently open" do + let!(:order_cycle) { current_order_cycle } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the order cycle opens in the future" do + let!(:order_cycle) { future_order_cycle } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the order cycle closed in the past" do + let!(:order_cycle) { past_order_cycle } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + end + end + + context "if the variant is unrelated" do + it "is not eligible" do + expect(described_class.eligible_variants(shop)).to_not include(variant) + end + end + end + + describe "checking if variant in open and upcoming order cycles" do + let!(:shop) { create(:enterprise) } + let!(:product) { create(:product) } + let!(:variant) { product.variants.first } + let!(:schedule) { create(:schedule) } + + context "if the variant is involved in an exchange" do + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + + context "if it is an incoming exchange where the shop is the receiver" do + let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) } + + it "is is false" do + expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop, schedule, variant) + end + end + + context "if it is an outgoing exchange where the shop is the receiver" do + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) } + + it "is true" do + expect(described_class).to be_in_open_and_upcoming_order_cycles(shop, schedule, variant) + end + end + end + + context "if the variant is unrelated" do + it "is false" do + expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop, schedule, variant) + end + end + end + end + end +end diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index ddf1d48771..5d16eafefe 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -61,7 +61,7 @@ module OpenFoodNetwork end def scope_to_eligible_for_subscriptions_in_distributor - eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor) + eligible_variants_scope = OrderManagement::Subscriptions::VariantsList.eligible_variants(distributor) @variants = @variants.merge(eligible_variants_scope) scope_variants_to_distributor(@variants, distributor) end diff --git a/spec/services/subscription_estimator_spec.rb b/spec/services/subscription_estimator_spec.rb deleted file mode 100644 index 5230057888..0000000000 --- a/spec/services/subscription_estimator_spec.rb +++ /dev/null @@ -1,129 +0,0 @@ -describe SubscriptionEstimator do - describe "estimating prices for subscription line items" do - let!(:subscription) { create(:subscription, with_items: true) } - let!(:sli1) { subscription.subscription_line_items.first } - let!(:sli2) { subscription.subscription_line_items.second } - let!(:sli3) { subscription.subscription_line_items.third } - let(:estimator) { SubscriptionEstimator.new(subscription) } - - before do - sli1.update_attributes(price_estimate: 4.0) - sli2.update_attributes(price_estimate: 5.0) - sli3.update_attributes(price_estimate: 6.0) - sli1.variant.update_attributes(price: 1.0) - sli2.variant.update_attributes(price: 2.0) - sli3.variant.update_attributes(price: 3.0) - - # Simulating assignment of attrs from params - sli1.assign_attributes(price_estimate: 7.0) - sli2.assign_attributes(price_estimate: 8.0) - sli3.assign_attributes(price_estimate: 9.0) - end - - context "when a insufficient information exists to calculate price estimates" do - before do - # This might be because a shop has not been assigned yet, or no - # current or future order cycles exist for the schedule - allow(estimator).to receive(:fee_calculator) { nil } - end - - it "resets the price estimates for all items" do - estimator.estimate! - expect(sli1.price_estimate).to eq 4.0 - expect(sli2.price_estimate).to eq 5.0 - expect(sli3.price_estimate).to eq 6.0 - end - end - - context "when sufficient information to calculate price estimates exists" do - let(:fee_calculator) { instance_double(OpenFoodNetwork::EnterpriseFeeCalculator) } - - before do - allow(estimator).to receive(:fee_calculator) { fee_calculator } - allow(fee_calculator).to receive(:indexed_fees_for).with(sli1.variant) { 1.0 } - allow(fee_calculator).to receive(:indexed_fees_for).with(sli2.variant) { 0.0 } - allow(fee_calculator).to receive(:indexed_fees_for).with(sli3.variant) { 3.0 } - end - - context "when no variant overrides apply" do - it "recalculates price_estimates based on variant prices and associated fees" do - estimator.estimate! - expect(sli1.price_estimate).to eq 2.0 - expect(sli2.price_estimate).to eq 2.0 - expect(sli3.price_estimate).to eq 6.0 - end - end - - context "when variant overrides apply" do - let!(:override1) { create(:variant_override, hub: subscription.shop, variant: sli1.variant, price: 1.2) } - let!(:override2) { create(:variant_override, hub: subscription.shop, variant: sli2.variant, price: 2.3) } - - it "recalculates price_estimates based on override prices and associated fees" do - estimator.estimate! - expect(sli1.price_estimate).to eq 2.2 - expect(sli2.price_estimate).to eq 2.3 - expect(sli3.price_estimate).to eq 6.0 - end - end - end - end - - describe "updating estimates for shipping and payment fees" do - let(:subscription) { create(:subscription, with_items: true, payment_method: payment_method, shipping_method: shipping_method) } - let!(:sli1) { subscription.subscription_line_items.first } - let!(:sli2) { subscription.subscription_line_items.second } - let!(:sli3) { subscription.subscription_line_items.third } - let(:estimator) { SubscriptionEstimator.new(subscription) } - - before do - allow(estimator).to receive(:assign_price_estimates) - sli1.update_attributes(price_estimate: 4.0) - sli2.update_attributes(price_estimate: 5.0) - sli3.update_attributes(price_estimate: 6.0) - end - - context "using flat rate calculators" do - let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 12.34)) } - let(:payment_method) { create(:payment_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 9.12)) } - - it "calculates fees based on the rates provided" do - estimator.estimate! - expect(subscription.shipping_fee_estimate.to_f).to eq 12.34 - expect(subscription.payment_fee_estimate.to_f).to eq 9.12 - end - end - - context "using flat percent item total calculators" do - let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } - let(:payment_method) { create(:payment_method, calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) } - - it "calculates fees based on the estimated item total and percentage provided" do - estimator.estimate! - expect(subscription.shipping_fee_estimate.to_f).to eq 1.5 - expect(subscription.payment_fee_estimate.to_f).to eq 3.0 - end - end - - context "using flat percent per item calculators" do - let(:shipping_method) { create(:shipping_method, calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 5)) } - let(:payment_method) { create(:payment_method, calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10)) } - - it "calculates fees based on the estimated item prices and percentage provided" do - estimator.estimate! - expect(subscription.shipping_fee_estimate.to_f).to eq 0.75 - expect(subscription.payment_fee_estimate.to_f).to eq 1.5 - end - end - - context "using per item calculators" do - let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::PerItem.new(preferred_amount: 1.2)) } - let(:payment_method) { create(:payment_method, calculator: Spree::Calculator::PerItem.new(preferred_amount: 0.3)) } - - it "calculates fees based on the number of items and rate provided" do - estimator.estimate! - expect(subscription.shipping_fee_estimate.to_f).to eq 3.6 - expect(subscription.payment_fee_estimate.to_f).to eq 0.9 - end - end - end -end diff --git a/spec/services/subscription_form_spec.rb b/spec/services/subscription_form_spec.rb deleted file mode 100644 index 9a06ef7164..0000000000 --- a/spec/services/subscription_form_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -require 'spec_helper' - -describe SubscriptionForm do - describe "creating a new subscription" do - let!(:shop) { create(:distributor_enterprise) } - let!(:customer) { create(:customer, enterprise: shop) } - let!(:product1) { create(:product, supplier: shop) } - let!(:product2) { create(:product, supplier: shop) } - let!(:product3) { create(:product, supplier: shop) } - let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) } - let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) } - let!(:variant3) { create(:variant, product: product2, unit_value: '1000', price: 2.50, option_values: [], on_hand: 1) } - let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) } - let!(:order_cycle1) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 9.days.ago, orders_close_at: 2.days.ago) } - let!(:order_cycle2) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.ago, orders_close_at: 5.days.from_now) } - let!(:order_cycle3) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 5.days.from_now, orders_close_at: 12.days.from_now) } - let!(:order_cycle4) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 12.days.from_now, orders_close_at: 19.days.from_now) } - let!(:outgoing_exchange1) { order_cycle1.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2, variant3], enterprise_fees: [enterprise_fee]) } - let!(:outgoing_exchange2) { order_cycle2.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2, variant3], enterprise_fees: [enterprise_fee]) } - let!(:outgoing_exchange3) { order_cycle3.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant3], enterprise_fees: []) } - let!(:outgoing_exchange4) { order_cycle4.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2, variant3], enterprise_fees: [enterprise_fee]) } - let!(:schedule) { create(:schedule, order_cycles: [order_cycle1, order_cycle2, order_cycle3, order_cycle4]) } - let!(:payment_method) { create(:payment_method, distributors: [shop]) } - let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } - let!(:address) { create(:address) } - let(:subscription) { Subscription.new } - - let!(:params) { - { - shop_id: shop.id, - customer_id: customer.id, - schedule_id: schedule.id, - bill_address_attributes: address.clone.attributes, - ship_address_attributes: address.clone.attributes, - payment_method_id: payment_method.id, - shipping_method_id: shipping_method.id, - begins_at: 4.days.ago, - ends_at: 14.days.from_now, - subscription_line_items_attributes: [ - { variant_id: variant1.id, quantity: 1, price_estimate: 7.0 }, - { variant_id: variant2.id, quantity: 2, price_estimate: 8.0 }, - { variant_id: variant3.id, quantity: 3, price_estimate: 9.0 } - ] - } - } - - let(:form) { SubscriptionForm.new(subscription, params) } - - it "creates orders for each order cycle in the schedule" do - expect(form.save).to be true - - expect(subscription.proxy_orders.count).to be 2 - expect(subscription.subscription_line_items.count).to be 3 - expect(subscription.subscription_line_items[0].price_estimate).to eq 13.75 - expect(subscription.subscription_line_items[1].price_estimate).to eq 7.75 - expect(subscription.subscription_line_items[2].price_estimate).to eq 4.25 - - # This order cycle has already closed, so no order is initialized - proxy_order1 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle1.id) - expect(proxy_order1).to be nil - - # Currently open order cycle, closing after begins_at and before ends_at - proxy_order2 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle2.id) - expect(proxy_order2).to be_a ProxyOrder - order2 = proxy_order2.initialise_order! - expect(order2.line_items.count).to eq 3 - expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3 - expect(order2.shipments.count).to eq 1 - expect(order2.shipments.first.shipping_method).to eq shipping_method - expect(order2.payments.count).to eq 1 - expect(order2.payments.first.payment_method).to eq payment_method - expect(order2.payments.first.state).to eq 'checkout' - expect(order2.total).to eq 42 - expect(order2.completed?).to be false - - # Future order cycle, closing after begins_at and before ends_at - # Adds line items for variants that aren't yet available from the order cycle - proxy_order3 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle3.id) - expect(proxy_order3).to be_a ProxyOrder - order3 = proxy_order3.initialise_order! - expect(order3).to be_a Spree::Order - expect(order3.line_items.count).to eq 3 - expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3 - expect(order3.shipments.count).to eq 1 - expect(order3.shipments.first.shipping_method).to eq shipping_method - expect(order3.payments.count).to eq 1 - expect(order3.payments.first.payment_method).to eq payment_method - expect(order3.payments.first.state).to eq 'checkout' - expect(order3.total).to eq 31.50 - expect(order3.completed?).to be false - - # Future order cycle closing after ends_at - proxy_order4 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle4.id) - expect(proxy_order4).to be nil - end - end -end diff --git a/spec/services/subscription_validator_spec.rb b/spec/services/subscription_validator_spec.rb deleted file mode 100644 index bdcd14bea5..0000000000 --- a/spec/services/subscription_validator_spec.rb +++ /dev/null @@ -1,470 +0,0 @@ -require "spec_helper" - -describe SubscriptionValidator do - let(:owner) { create(:user) } - let(:shop) { create(:enterprise, name: "Shop", owner: owner) } - - describe "delegation" do - let(:subscription) { create(:subscription, shop: shop) } - let(:validator) { SubscriptionValidator.new(subscription) } - - it "delegates to subscription" do - expect(validator.shop).to eq subscription.shop - expect(validator.customer).to eq subscription.customer - expect(validator.schedule).to eq subscription.schedule - expect(validator.shipping_method).to eq subscription.shipping_method - expect(validator.payment_method).to eq subscription.payment_method - expect(validator.bill_address).to eq subscription.bill_address - expect(validator.ship_address).to eq subscription.ship_address - expect(validator.begins_at).to eq subscription.begins_at - expect(validator.ends_at).to eq subscription.ends_at - end - end - - describe "validations" do - let(:subscription_stubs) do - { - shop: shop, - customer: true, - schedule: true, - shipping_method: true, - payment_method: true, - bill_address: true, - ship_address: true, - begins_at: true, - ends_at: true, - } - end - - let(:validation_stubs) do - { - shipping_method_allowed?: true, - payment_method_allowed?: true, - payment_method_type_allowed?: true, - ends_at_after_begins_at?: true, - customer_allowed?: true, - schedule_allowed?: true, - credit_card_ok?: true, - subscription_line_items_present?: true, - requested_variants_available?: true - } - end - - let(:subscription) { instance_double(Subscription, subscription_stubs) } - let(:validator) { SubscriptionValidator.new(subscription) } - - def stub_validations(validator, methods) - methods.each do |name, value| - allow(validator).to receive(name) { value } - end - end - - describe "shipping method validation" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:shipping_method)) } - before { stub_validations(validator, validation_stubs.except(:shipping_method_allowed?)) } - - context "when no shipping method is present" do - before { expect(subscription).to receive(:shipping_method).at_least(:once) { nil } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:shipping_method]).to_not be_empty - end - end - - context "when a shipping method is present" do - let(:shipping_method) { instance_double(Spree::ShippingMethod, distributors: [shop]) } - before { expect(subscription).to receive(:shipping_method).at_least(:once) { shipping_method } } - - context "and the shipping method is not associated with the shop" do - before { allow(shipping_method).to receive(:distributors) { [double(:enterprise)] } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:shipping_method]).to_not be_empty - end - end - - context "and the shipping method is associated with the shop" do - before { allow(shipping_method).to receive(:distributors) { [shop] } } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:shipping_method]).to be_empty - end - end - end - end - - describe "payment method validation" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:payment_method)) } - before { stub_validations(validator, validation_stubs.except(:payment_method_allowed?)) } - - context "when no payment method is present" do - before { expect(subscription).to receive(:payment_method).at_least(:once) { nil } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:payment_method]).to_not be_empty - end - end - - context "when a payment method is present" do - let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) } - before { expect(subscription).to receive(:payment_method).at_least(:once) { payment_method } } - - context "and the payment method is not associated with the shop" do - before { allow(payment_method).to receive(:distributors) { [double(:enterprise)] } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:payment_method]).to_not be_empty - end - end - - context "and the payment method is associated with the shop" do - before { allow(payment_method).to receive(:distributors) { [shop] } } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:payment_method]).to be_empty - end - end - end - end - - describe "payment method type validation" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:payment_method)) } - before { stub_validations(validator, validation_stubs.except(:payment_method_type_allowed?)) } - - context "when a payment method is present" do - let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) } - before { expect(subscription).to receive(:payment_method).at_least(:once) { payment_method } } - - context "and the payment method type is not in the approved list" do - before { allow(payment_method).to receive(:type) { "Blah" } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:payment_method]).to_not be_empty - end - end - - context "and the payment method is in the approved list" do - let(:approved_type) { Subscription::ALLOWED_PAYMENT_METHOD_TYPES.first } - before { allow(payment_method).to receive(:type) { approved_type } } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:payment_method]).to be_empty - end - end - end - end - - describe "dates" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:begins_at, :ends_at)) } - before { stub_validations(validator, validation_stubs.except(:ends_at_after_begins_at?)) } - before { expect(subscription).to receive(:begins_at).at_least(:once) { begins_at } } - - context "when no begins_at is present" do - let(:begins_at) { nil } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:begins_at]).to_not be_empty - end - end - - context "when a start date is present" do - let(:begins_at) { Time.zone.today } - before { expect(subscription).to receive(:ends_at).at_least(:once) { ends_at } } - - context "when no ends_at is present" do - let(:ends_at) { nil } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:ends_at]).to be_empty - end - end - - context "when ends_at is equal to begins_at" do - let(:ends_at) { Time.zone.today } - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:ends_at]).to_not be_empty - end - end - - context "when ends_at is before begins_at" do - let(:ends_at) { Time.zone.today - 1.day } - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:ends_at]).to_not be_empty - end - end - - context "when ends_at is after begins_at" do - let(:ends_at) { Time.zone.today + 1.day } - it "adds an error and returns false" do - expect(validator.valid?).to be true - expect(validator.errors[:ends_at]).to be_empty - end - end - end - end - - describe "addresses" do - before { stub_validations(validator, validation_stubs) } - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:bill_address, :ship_address)) } - before { expect(subscription).to receive(:bill_address).at_least(:once) { bill_address } } - before { expect(subscription).to receive(:ship_address).at_least(:once) { ship_address } } - - context "when bill_address and ship_address are not present" do - let(:bill_address) { nil } - let(:ship_address) { nil } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:bill_address]).to_not be_empty - expect(validator.errors[:ship_address]).to_not be_empty - end - end - - context "when bill_address and ship_address are present" do - let(:bill_address) { instance_double(Spree::Address) } - let(:ship_address) { instance_double(Spree::Address) } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:bill_address]).to be_empty - expect(validator.errors[:ship_address]).to be_empty - end - end - end - - describe "customer" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:customer)) } - before { stub_validations(validator, validation_stubs.except(:customer_allowed?)) } - before { expect(subscription).to receive(:customer).at_least(:once) { customer } } - - context "when no customer is present" do - let(:customer) { nil } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:customer]).to_not be_empty - end - end - - context "when a customer is present" do - let(:customer) { instance_double(Customer) } - - context "and the customer is not associated with the shop" do - before { allow(customer).to receive(:enterprise) { double(:enterprise) } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:customer]).to_not be_empty - end - end - - context "and the customer is associated with the shop" do - before { allow(customer).to receive(:enterprise) { shop } } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:customer]).to be_empty - end - end - end - end - - describe "schedule" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:schedule)) } - before { stub_validations(validator, validation_stubs.except(:schedule_allowed?)) } - before { expect(subscription).to receive(:schedule).at_least(:once) { schedule } } - - context "when no schedule is present" do - let(:schedule) { nil } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:schedule]).to_not be_empty - end - end - - context "when a schedule is present" do - let(:schedule) { instance_double(Schedule) } - - context "and the schedule is not associated with the shop" do - before { allow(schedule).to receive(:coordinators) { [double(:enterprise)] } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:schedule]).to_not be_empty - end - end - - context "and the schedule is associated with the shop" do - before { allow(schedule).to receive(:coordinators) { [shop] } } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:schedule]).to be_empty - end - end - end - end - - describe "credit card" do - let(:subscription) { instance_double(Subscription, subscription_stubs.except(:payment_method)) } - before { stub_validations(validator, validation_stubs.except(:credit_card_ok?)) } - before { expect(subscription).to receive(:payment_method).at_least(:once) { payment_method } } - - context "when using a Check payment method" do - let(:payment_method) { instance_double(Spree::PaymentMethod, type: "Spree::PaymentMethod::Check") } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:subscription_line_items]).to be_empty - end - end - - context "when using the StripeConnect payment gateway" do - let(:payment_method) { instance_double(Spree::PaymentMethod, type: "Spree::Gateway::StripeConnect") } - before { expect(subscription).to receive(:customer).at_least(:once) { customer } } - - context "when the customer does not allow charges" do - let(:customer) { instance_double(Customer, allow_charges: false) } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:payment_method]).to_not be_empty - end - end - - context "when the customer allows charges" do - let(:customer) { instance_double(Customer, allow_charges: true) } - - context "and the customer is not associated with a user" do - before { allow(customer).to receive(:user) { nil } } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:payment_method]).to_not be_empty - end - end - - context "and the customer is associated with a user" do - before { expect(customer).to receive(:user).once { user } } - - context "and the user has no default card set" do - let(:user) { instance_double(Spree::User, default_card: nil) } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:payment_method]).to_not be_empty - end - end - - context "and the user has a default card set" do - let(:user) { instance_double(Spree::User, default_card: 'some card') } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:payment_method]).to be_empty - end - end - end - end - end - end - - describe "subscription line items" do - let(:subscription) { instance_double(Subscription, subscription_stubs) } - before { stub_validations(validator, validation_stubs.except(:subscription_line_items_present?)) } - before { expect(subscription).to receive(:subscription_line_items).at_least(:once) { subscription_line_items } } - - context "when no subscription line items are present" do - let(:subscription_line_items) { [] } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:subscription_line_items]).to_not be_empty - end - end - - context "when subscription line items are present but they are all marked for destruction" do - let(:subscription_line_item1) { instance_double(SubscriptionLineItem, marked_for_destruction?: true) } - let(:subscription_line_items) { [subscription_line_item1] } - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:subscription_line_items]).to_not be_empty - end - end - - context "when subscription line items are present and some and not marked for destruction" do - let(:subscription_line_item1) { instance_double(SubscriptionLineItem, marked_for_destruction?: true) } - let(:subscription_line_item2) { instance_double(SubscriptionLineItem, marked_for_destruction?: false) } - let(:subscription_line_items) { [subscription_line_item1, subscription_line_item2] } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:subscription_line_items]).to be_empty - end - end - end - - describe "variant availability" do - let(:subscription) { instance_double(Subscription, subscription_stubs) } - before { stub_validations(validator, validation_stubs.except(:requested_variants_available?)) } - before { expect(subscription).to receive(:subscription_line_items).at_least(:once) { subscription_line_items } } - - context "when no subscription line items are present" do - let(:subscription_line_items) { [] } - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:subscription_line_items]).to be_empty - end - end - - context "when subscription line items are present" do - let(:variant1) { instance_double(Spree::Variant, id: 1) } - let(:variant2) { instance_double(Spree::Variant, id: 2) } - let(:subscription_line_item1) { instance_double(SubscriptionLineItem, variant: variant1) } - let(:subscription_line_item2) { instance_double(SubscriptionLineItem, variant: variant2) } - let(:subscription_line_items) { [subscription_line_item1] } - - context "but some variants are unavailable" do - let(:product) { instance_double(Spree::Product, name: "some_name") } - - before do - allow(validator).to receive(:available_variant_ids) { [variant2.id] } - allow(variant1).to receive(:product) { product } - allow(variant1).to receive(:full_name) { "some name" } - end - - it "adds an error and returns false" do - expect(validator.valid?).to be false - expect(validator.errors[:subscription_line_items]).to_not be_empty - end - end - - context "and all requested variants are available" do - before do - allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } - end - - it "returns true" do - expect(validator.valid?).to be true - expect(validator.errors[:subscription_line_items]).to be_empty - end - end - end - end - end -end diff --git a/spec/services/subscription_variants_service_spec.rb b/spec/services/subscription_variants_service_spec.rb deleted file mode 100644 index 31d0ff4ca7..0000000000 --- a/spec/services/subscription_variants_service_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -require "spec_helper" - -describe SubscriptionVariantsService do - describe "variant eligibility for subscription" do - let!(:shop) { create(:distributor_enterprise) } - let!(:producer) { create(:supplier_enterprise) } - let!(:product) { create(:product, supplier: producer) } - let!(:variant) { product.variants.first } - - let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } - let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) } - let!(:subscription_line_item) do - create(:subscription_line_item, subscription: subscription, variant: variant) - end - - let(:current_order_cycle) do - create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago, - orders_close_at: 1.week.from_now) - end - - let(:future_order_cycle) do - create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now, - orders_close_at: 2.weeks.from_now) - end - - let(:past_order_cycle) do - create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago, - orders_close_at: 1.week.ago) - end - - let!(:order_cycle) { current_order_cycle } - - context "if the shop is the supplier for the product" do - let!(:producer) { shop } - - it "is eligible" do - expect(described_class.eligible_variants(shop)).to include(variant) - end - end - - context "if the supplier is permitted for the shop" do - let!(:enterprise_relationship) { create(:enterprise_relationship, child: shop, parent: product.supplier, permissions_list: [:add_to_order_cycle]) } - - it "is eligible" do - expect(described_class.eligible_variants(shop)).to include(variant) - end - end - - context "if the variant is involved in an exchange" do - let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } - let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } - - context "if it is an incoming exchange where the shop is the receiver" do - let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) } - - it "is not eligible" do - expect(described_class.eligible_variants(shop)).to_not include(variant) - end - end - - context "if it is an outgoing exchange where the shop is the receiver" do - let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) } - - context "if the order cycle is currently open" do - let!(:order_cycle) { current_order_cycle } - - it "is eligible" do - expect(described_class.eligible_variants(shop)).to include(variant) - end - end - - context "if the order cycle opens in the future" do - let!(:order_cycle) { future_order_cycle } - - it "is eligible" do - expect(described_class.eligible_variants(shop)).to include(variant) - end - end - - context "if the order cycle closed in the past" do - let!(:order_cycle) { past_order_cycle } - - it "is eligible" do - expect(described_class.eligible_variants(shop)).to include(variant) - end - end - end - end - - context "if the variant is unrelated" do - it "is not eligible" do - expect(described_class.eligible_variants(shop)).to_not include(variant) - end - end - end - - describe "checking if variant in open and upcoming order cycles" do - let!(:shop) { create(:enterprise) } - let!(:product) { create(:product) } - let!(:variant) { product.variants.first } - let!(:schedule) { create(:schedule) } - - context "if the variant is involved in an exchange" do - let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } - let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } - - context "if it is an incoming exchange where the shop is the receiver" do - let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) } - - it "is is false" do - expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop, schedule, variant) - end - end - - context "if it is an outgoing exchange where the shop is the receiver" do - let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) } - - it "is true" do - expect(described_class).to be_in_open_and_upcoming_order_cycles(shop, schedule, variant) - end - end - end - - context "if the variant is unrelated" do - it "is false" do - expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop, schedule, variant) - end - end - end -end diff --git a/spec/services/subscriptions_count_spec.rb b/spec/services/subscriptions_count_spec.rb deleted file mode 100644 index 2446bcc8bf..0000000000 --- a/spec/services/subscriptions_count_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -describe SubscriptionsCount do - let(:oc1) { create(:simple_order_cycle) } - let(:oc2) { create(:simple_order_cycle) } - let(:subscriptions_count) { SubscriptionsCount.new(order_cycles) } - - describe "#for" do - context "when the collection has not been set" do - let(:order_cycles) { nil } - it "returns 0" do - expect(subscriptions_count.for(oc1.id)).to eq 0 - end - end - - context "when the collection has been set" do - let(:order_cycles) { OrderCycle.where(id: [oc1]) } - let!(:po1) { create(:proxy_order, order_cycle: oc1) } - let!(:po2) { create(:proxy_order, order_cycle: oc1) } - let!(:po3) { create(:proxy_order, order_cycle: oc2) } - - context "but the requested id is not present in the list of order cycles provided" do - it "returns 0" do - # Note that po3 applies to oc2, but oc2 in not in the collection - expect(subscriptions_count.for(oc2.id)).to eq 0 - end - end - - context "and the requested id is present in the list of order cycles provided" do - it "returns a count of active proxy orders associated with the requested order cycle" do - expect(subscriptions_count.for(oc1.id)).to eq 2 - end - end - end - end -end