Move 5 subscriptions services from app/services to the engines/order_management/app/services

This commit is contained in:
Luis Ramos
2020-02-11 19:34:14 +00:00
parent f68d0c2a0f
commit 29377bbff9
28 changed files with 1219 additions and 1169 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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