mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-29 21:17:17 +00:00
Move 5 subscriptions services from app/services to the engines/order_management/app/services
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user