Merge pull request #4783 from luisramos0/stripe_sca_extra_subs

Move all subscriptions services to the OrderManagement engine
This commit is contained in:
Luis Ramos
2020-04-06 20:20:02 +01:00
committed by GitHub
53 changed files with 2742 additions and 2446 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
@@ -239,10 +238,7 @@ Layout/LineLength:
- spec/lib/open_food_network/packing_report_spec.rb
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
- spec/lib/open_food_network/subscription_summarizer_spec.rb
- spec/lib/open_food_network/tag_rule_applicator_spec.rb
- spec/lib/open_food_network/users_and_enterprises_report_spec.rb
- spec/lib/open_food_network/xero_invoices_report_spec.rb
@@ -317,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
@@ -411,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
@@ -686,6 +678,13 @@ Metrics/ModuleLength:
- app/helpers/injection_helper.rb
- app/helpers/spree/admin/navigation_helper.rb
- app/helpers/spree/admin/base_helper.rb
- engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/form_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/proxy_order_syncer_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/payment_setup_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/summarizer_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb
- lib/open_food_network/column_preference_defaults.rb
- spec/controllers/admin/enterprises_controller_spec.rb
- spec/controllers/admin/order_cycles_controller_spec.rb
@@ -701,9 +700,7 @@ Metrics/ModuleLength:
- spec/lib/open_food_network/order_grouper_spec.rb
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
- spec/lib/open_food_network/tag_rule_applicator_spec.rb
- spec/lib/open_food_network/users_and_enterprises_report_spec.rb
- spec/models/spree/ability_spec.rb

View File

@@ -71,7 +71,6 @@ Lint/DuplicateHashKey:
Lint/DuplicateMethods:
Exclude:
- 'lib/discourse/single_sign_on.rb'
- 'lib/open_food_network/subscription_summary.rb'
# Offense count: 10
Lint/IneffectiveAccessModifier:
@@ -159,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 +848,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'
@@ -892,6 +886,7 @@ Style/FrozenStringLiteralComment:
- 'engines/order_management/lib/order_management/engine.rb'
- 'engines/order_management/lib/order_management/version.rb'
- 'engines/order_management/order_management.gemspec'
- 'engines/order_management/spec/performance/order_management/subscriptions/proxy_order_syncer_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/authorizer_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/parameters_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/permissions_spec.rb'
@@ -949,7 +944,6 @@ Style/FrozenStringLiteralComment:
- 'lib/open_food_network/products_and_inventory_report.rb'
- 'lib/open_food_network/products_and_inventory_report_base.rb'
- 'lib/open_food_network/property_merge.rb'
- 'lib/open_food_network/proxy_order_syncer.rb'
- 'lib/open_food_network/rack_request_blocker.rb'
- 'lib/open_food_network/referer_parser.rb'
- 'lib/open_food_network/reports/bulk_coop_allocation_report.rb'
@@ -966,8 +960,6 @@ Style/FrozenStringLiteralComment:
- 'lib/open_food_network/scope_variants_for_search.rb'
- 'lib/open_food_network/spree_api_key_loader.rb'
- 'lib/open_food_network/subscription_payment_updater.rb'
- 'lib/open_food_network/subscription_summarizer.rb'
- 'lib/open_food_network/subscription_summary.rb'
- 'lib/open_food_network/tag_rule_applicator.rb'
- 'lib/open_food_network/user_balance_calculator.rb'
- 'lib/open_food_network/users_and_enterprises_report.rb'
@@ -1209,7 +1201,6 @@ Style/FrozenStringLiteralComment:
- 'spec/lib/open_food_network/permissions_spec.rb'
- 'spec/lib/open_food_network/products_and_inventory_report_spec.rb'
- 'spec/lib/open_food_network/property_merge_spec.rb'
- 'spec/lib/open_food_network/proxy_order_syncer_spec.rb'
- 'spec/lib/open_food_network/referer_parser_spec.rb'
- 'spec/lib/open_food_network/reports/report_spec.rb'
- 'spec/lib/open_food_network/reports/row_spec.rb'
@@ -1218,8 +1209,6 @@ Style/FrozenStringLiteralComment:
- 'spec/lib/open_food_network/scope_variant_to_hub_spec.rb'
- 'spec/lib/open_food_network/scope_variants_to_search_spec.rb'
- 'spec/lib/open_food_network/subscription_payment_updater_spec.rb'
- 'spec/lib/open_food_network/subscription_summarizer_spec.rb'
- 'spec/lib/open_food_network/subscription_summary_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/open_food_network/user_balance_calculator_spec.rb'
- 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb'
@@ -1304,7 +1293,6 @@ Style/FrozenStringLiteralComment:
- 'spec/models/variant_override_spec.rb'
- 'spec/performance/injection_helper_spec.rb'
- 'spec/performance/orders_controller_spec.rb'
- 'spec/performance/proxy_order_syncer_spec.rb'
- 'spec/performance/shop_controller_spec.rb'
- 'spec/requests/checkout/failed_checkout_spec.rb'
- 'spec/requests/checkout/paypal_spec.rb'
@@ -1355,11 +1343,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'
@@ -1557,7 +1540,6 @@ Style/Send:
- 'spec/lib/open_food_network/products_and_inventory_report_spec.rb'
- 'spec/lib/open_food_network/sales_tax_report_spec.rb'
- 'spec/lib/open_food_network/subscription_payment_updater_spec.rb'
- 'spec/lib/open_food_network/subscription_summarizer_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/open_food_network/xero_invoices_report_spec.rb'
- 'spec/lib/stripe/webhook_handler_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

@@ -1,5 +1,5 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
require 'order_management/subscriptions/proxy_order_syncer'
module Admin
class SchedulesController < ResourceController
@@ -81,7 +81,7 @@ module Admin
return unless removed_ids.any? || new_ids.any?
subscriptions = Subscription.where(schedule_id: @schedule)
syncer = OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions)
syncer = OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions)
syncer.sync!
end
end

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

@@ -1,5 +1,4 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
module Admin
class SubscriptionsController < ResourceController
@@ -65,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

@@ -1,17 +1,9 @@
require 'open_food_network/subscription_payment_updater'
require 'open_food_network/subscription_summarizer'
require 'order_management/subscriptions/summarizer'
# Confirms orders of unconfirmed proxy orders in recently closed Order Cycles
class SubscriptionConfirmJob
def perform
ids = proxy_orders.pluck(:id)
proxy_orders.update_all(confirmed_at: Time.zone.now)
ProxyOrder.where(id: ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
@order = proxy_order.order
process!
end
send_confirmation_summary_emails
confirm_proxy_orders!
end
private
@@ -20,10 +12,26 @@ class SubscriptionConfirmJob
delegate :record_and_log_error, :send_confirmation_summary_emails, to: :summarizer
def summarizer
@summarizer ||= OpenFoodNetwork::SubscriptionSummarizer.new
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
end
def proxy_orders
def confirm_proxy_orders!
# Fetch all unconfirmed proxy orders
unconfirmed_proxy_orders_ids = unconfirmed_proxy_orders.pluck(:id)
# Mark these proxy orders as confirmed
unconfirmed_proxy_orders.update_all(confirmed_at: Time.zone.now)
# Confirm these proxy orders
ProxyOrder.where(id: unconfirmed_proxy_orders_ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
confirm_order!(proxy_order.order)
end
send_confirmation_summary_emails
end
def unconfirmed_proxy_orders
ProxyOrder.not_canceled.where('confirmed_at IS NULL AND placed_at IS NOT NULL')
.joins(:order_cycle).merge(recently_closed_order_cycles)
.joins(:order).merge(Spree::Order.complete.not_state('canceled'))
@@ -33,30 +41,43 @@ class SubscriptionConfirmJob
OrderCycle.closed.where('order_cycles.orders_close_at BETWEEN (?) AND (?) OR order_cycles.updated_at BETWEEN (?) AND (?)', 1.hour.ago, Time.zone.now, 1.hour.ago, Time.zone.now)
end
def process!
record_order(@order)
update_payment! if @order.payment_required?
return send_failed_payment_email if @order.errors.present?
# It sets up payments, processes payments and sends confirmation emails
def confirm_order!(order)
record_order(order)
@order.process_payments! if @order.payment_required?
return send_failed_payment_email if @order.errors.present?
send_confirm_email
if process_payment!(order)
send_confirmation_email(order)
else
send_failed_payment_email(order)
end
end
def update_payment!
OpenFoodNetwork::SubscriptionPaymentUpdater.new(@order).update!
def process_payment!(order)
return false if order.errors.present?
return true unless order.payment_required?
setup_payment!(order)
return false if order.errors.present?
order.process_payments!
return false if order.errors.present?
true
end
def send_confirm_email
@order.update!
record_success(@order)
SubscriptionMailer.confirmation_email(@order).deliver
def setup_payment!(order)
OrderManagement::Subscriptions::PaymentSetup.new(order).call!
end
def send_failed_payment_email
@order.update!
record_and_log_error(:failed_payment, @order)
SubscriptionMailer.failed_payment_email(@order).deliver
def send_confirmation_email(order)
order.update!
record_success(order)
SubscriptionMailer.confirmation_email(order).deliver
end
def send_failed_payment_email(order)
order.update!
record_and_log_error(:failed_payment, order)
SubscriptionMailer.failed_payment_email(order).deliver
end
end

View File

@@ -1,4 +1,4 @@
require 'open_food_network/subscription_summarizer'
require 'order_management/subscriptions/summarizer'
class SubscriptionPlacementJob
def perform
@@ -17,7 +17,7 @@ class SubscriptionPlacementJob
delegate :record_and_log_error, :send_placement_summary_emails, to: :summarizer
def summarizer
@summarizer ||= OpenFoodNetwork::SubscriptionSummarizer.new
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
end
def proxy_orders

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,6 +1,6 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
require 'open_food_network/order_cycle_form_applicator'
require 'order_management/subscriptions/proxy_order_syncer'
class OrderCycleForm
def initialize(order_cycle, params, user)
@@ -58,7 +58,7 @@ class OrderCycleForm
return unless schedule_ids?
return unless schedule_sync_required?
OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
end
def schedule_sync_required?

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 'open_food_network/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
OpenFoodNetwork::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

@@ -80,7 +80,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,69 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
class PaymentSetup
def initialize(order)
@order = order
end
def call!
create_payment
ensure_payment_source
return if order.errors.any?
payment.update_attributes(amount: order.outstanding_balance)
end
private
attr_reader :order
def payment
@payment ||= order.pending_payments.last
end
def create_payment
return if payment.present?
@payment = order.payments.create(
payment_method_id: order.subscription.payment_method_id,
amount: order.outstanding_balance
)
end
def card_required?
[Spree::Gateway::StripeConnect,
Spree::Gateway::StripeSCA].include? payment.payment_method.class
end
def card_set?
payment.source is_a? Spree::CreditCard
end
def ensure_payment_source
return unless card_required? && !card_set?
ensure_credit_card || order.errors.add(:base, :no_card)
end
def ensure_credit_card
return false if saved_credit_card.blank? || !allow_charges?
payment.update_attributes(source: saved_credit_card)
end
def allow_charges?
order.customer.allow_charges?
end
def saved_credit_card
order.user.default_card
end
def errors_present?
order.errors.any?
end
end
end
end

View File

@@ -0,0 +1,99 @@
# frozen_string_literal: false
module OrderManagement
module Subscriptions
class ProxyOrderSyncer
attr_reader :subscription
delegate :order_cycles, :proxy_orders, :begins_at, :ends_at, to: :subscription
def initialize(subscriptions)
case subscriptions
when Subscription
@subscription = subscriptions
when ActiveRecord::Relation
@subscriptions = subscriptions.not_ended.not_canceled
else
raise "ProxyOrderSyncer must be initialized with " \
"an instance of Subscription or ActiveRecord::Relation"
end
end
def sync!
return sync_subscriptions! if @subscriptions
return initialise_proxy_orders! unless @subscription.id
sync_subscription!
end
private
def sync_subscriptions!
@subscriptions.each do |subscription|
@subscription = subscription
sync_subscription!
end
end
def initialise_proxy_orders!
uninitialised_order_cycle_ids.each do |order_cycle_id|
Rails.logger.info "Initializing Proxy Order " \
"of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
end
end
def sync_subscription!
Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
create_proxy_orders!
remove_orphaned_proxy_orders!
end
def create_proxy_orders!
return unless not_closed_in_range_order_cycles.any?
query = "INSERT INTO proxy_orders (subscription_id, order_cycle_id, updated_at, created_at)"
query << " VALUES #{insert_values}"
query << " ON CONFLICT DO NOTHING"
ActiveRecord::Base.connection.exec_query(query)
end
def uninitialised_order_cycle_ids
not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id)
end
def remove_orphaned_proxy_orders!
orphaned_proxy_orders.scoped.delete_all
end
# Remove Proxy Orders that have not been placed yet
# and are in Order Cycles that are out of range
def orphaned_proxy_orders
orphaned = proxy_orders.where(placed_at: nil)
order_cycle_ids = in_range_order_cycles.pluck(:id)
return orphaned unless order_cycle_ids.any?
orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids)
end
def insert_values
now = Time.now.utc.iso8601
not_closed_in_range_order_cycles
.map{ |oc| "(#{subscription.id},#{oc.id},'#{now}','#{now}')" }
.join(",")
end
def not_closed_in_range_order_cycles
in_range_order_cycles.merge(OrderCycle.not_closed)
end
def in_range_order_cycles
order_cycles.where("orders_close_at >= ? AND orders_close_at <= ?",
begins_at,
ends_at || 100.years.from_now)
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
# Used by for SubscriptionPlacementJob and SubscriptionConfirmJob to summarize the
# result of automatic processing of subscriptions for the relevant shop owners.
module OrderManagement
module Subscriptions
class Summarizer
def initialize
@summaries = {}
end
def record_order(order)
summary_for(order).record_order(order)
end
def record_success(order)
summary_for(order).record_success(order)
end
def record_issue(type, order, message = nil)
Rails.logger.info "Issue in Subscription Order #{order.id}: #{type}"
summary_for(order).record_issue(type, order, message)
end
def record_and_log_error(type, order)
return record_issue(type, order) unless order.errors.any?
error = "Subscription#{type.to_s.camelize}Error"
line1 = "#{error}: Cannot process order #{order.number} due to errors"
line2 = "Errors: #{order.errors.full_messages.join(', ')}"
Rails.logger.info("#{line1}\n#{line2}")
record_issue(type, order, line2)
end
def send_placement_summary_emails
@summaries.values.each do |summary|
SubscriptionMailer.placement_summary_email(summary).deliver
end
end
def send_confirmation_summary_emails
@summaries.values.each do |summary|
SubscriptionMailer.confirmation_summary_email(summary).deliver
end
end
private
def summary_for(order)
shop_id = order.distributor_id
@summaries[shop_id] ||= Summary.new(shop_id)
end
end
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
class Summary
attr_reader :shop_id, :issues
def initialize(shop_id)
@shop_id = shop_id
@order_ids = []
@success_ids = []
@issues = {}
end
def record_order(order)
@order_ids << order.id
end
def record_success(order)
@success_ids << order.id
end
def record_issue(type, order, message)
issues[type] ||= {}
issues[type][order.id] = message
end
def order_count
@order_ids.count
end
def success_count
@success_ids.count
end
def issue_count
(@order_ids - @success_ids).count
end
def orders_affected_by(type)
case type
when :other then Spree::Order.where(id: unrecorded_ids)
else Spree::Order.where(id: issues[type].keys)
end
end
def unrecorded_ids
recorded_ids = issues.values.map(&:keys).flatten
@order_ids - @success_ids - recorded_ids
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,69 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe ProxyOrderSyncer, performance: true do
let(:start) { Time.zone.now.beginning_of_day }
let!(:schedule) { create(:schedule, order_cycles: order_cycles) }
let!(:order_cycles) do
Array.new(10) do |i|
create(:simple_order_cycle, orders_open_at: start + i.days,
orders_close_at: start + (i + 1).days )
end
end
let!(:subscriptions) do
Array.new(150) do |_i|
create(:subscription, schedule: schedule, begins_at: start, ends_at: start + 10.days)
end
Subscription.where(schedule_id: schedule)
end
context "measuring performance for initialisation" do
it "reports the average run time for adding 10 OCs to 150 subscriptions" do
expect(ProxyOrder.count).to be 0
times = []
10.times do
syncer = ProxyOrderSyncer.new(subscriptions.reload)
t1 = Time.zone.now
syncer.sync!
t2 = Time.zone.now
diff = t2 - t1
times << diff
puts diff.round(2)
expect(ProxyOrder.count).to be 1500
ProxyOrder.destroy_all
end
puts "AVG: #{(times.sum / times.count).round(2)}"
end
end
context "measuring performance for removal" do
it "reports the average run time for removing 8 OCs from 150 subscriptions" do
times = []
10.times do
syncer = ProxyOrderSyncer.new(subscriptions.reload)
syncer.sync!
expect(ProxyOrder.count).to be 1500
subscriptions.update_all(begins_at: start + 8.days + 1.minute)
syncer = ProxyOrderSyncer.new(subscriptions.reload)
t1 = Time.zone.now
syncer.sync!
t2 = Time.zone.now
diff = t2 - t1
times << diff
puts diff.round(2)
expect(ProxyOrder.count).to be 300
subscriptions.update_all(begins_at: start)
end
puts "AVG: #{(times.sum / times.count).round(2)}"
end
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,171 @@
# 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,148 @@
# 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,231 @@
# frozen_string_literal: true
require 'spec_helper'
module OrderManagement
module Subscriptions
describe PaymentSetup do
let(:order) { create(:order) }
let(:payment_setup) { OrderManagement::Subscriptions::PaymentSetup.new(order) }
describe "#payment" do
context "when only one payment exists on the order" do
let!(:payment) { create(:payment, order: order) }
context "where the payment is pending" do
it { expect(payment_setup.__send__(:payment)).to eq payment }
end
context "where the payment is failed" do
before { payment.update_attribute(:state, 'failed') }
it { expect(payment_setup.__send__(:payment)).to be nil }
end
end
context "when more that one payment exists on the order" do
let!(:payment1) { create(:payment, order: order) }
let!(:payment2) { create(:payment, order: order) }
context "where more than one payment is pending" do
it { expect([payment1, payment2]).to include payment_setup.__send__(:payment) }
end
context "where only one payment is pending" do
before { payment1.update_attribute(:state, 'failed') }
it { expect(payment_setup.__send__(:payment)).to eq payment2 }
end
context "where no payments are pending" do
before do
payment1.update_attribute(:state, 'failed')
payment2.update_attribute(:state, 'failed')
end
it { expect(payment_setup.__send__(:payment)).to be nil }
end
end
end
describe "#call!" do
let!(:payment){ create(:payment, amount: 10) }
context "when no pending payments are present" do
let(:payment_method) { create(:payment_method) }
let(:subscription) { double(:subscription, payment_method_id: payment_method.id) }
before do
allow(order).to receive(:pending_payments).once { [] }
allow(order).to receive(:outstanding_balance) { 5 }
allow(order).to receive(:subscription) { subscription }
end
it "creates a new payment on the order" do
expect{ payment_setup.call! }.to change(Spree::Payment, :count).by(1)
expect(order.payments.first.amount).to eq 5
end
end
context "when a payment is present" do
before { allow(order).to receive(:pending_payments).once { [payment] } }
context "when a credit card is not required" do
before do
allow(payment_setup).to receive(:card_required?) { false }
expect(payment_setup).to_not receive(:card_available?)
expect(payment_setup).to_not receive(:ensure_credit_card)
end
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
end
end
end
context "when a credit card is required" do
before do
expect(payment_setup).to receive(:card_required?) { true }
end
context "and the payment source is not a credit card" do
before { expect(payment_setup).to receive(:card_set?) { false } }
context "and no default credit card has been set by the customer" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
end
it "adds an error to the order and does not update the payment" do
expect(payment).to_not receive(:update_attributes)
expect{ payment_setup.call! }.to change(order.errors[:base], :count).from(0).to(1)
end
end
context "and the customer has not authorised the shop to charge to credit cards" do
before do
allow(order).to receive(:user) {
instance_double(Spree::User, default_card: create(:credit_card))
}
allow(order).to receive(:customer) {
instance_double(Customer, allow_charges?: false)
}
end
it "adds an error to the order and does not update the payment" do
expect(payment).to_not receive(:update_attributes)
expect{ payment_setup.call! }.to change(order.errors[:base], :count).from(0).to(1)
end
end
context "and an authorised default credit card is available to charge" do
before do
allow(order).to receive(:user) {
instance_double(Spree::User, default_card: create(:credit_card))
}
allow(order).to receive(:customer) {
instance_double(Customer, allow_charges?: true)
}
end
context "when the payment total doesn't match the order's outstanding balance" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
end
end
end
end
context "and the payment source is already a credit card" do
before { expect(payment_setup).to receive(:card_set?) { true } }
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
end
end
end
end
end
end
describe "#ensure_credit_card" do
let!(:payment) { create(:payment, source: nil) }
before { allow(payment_setup).to receive(:payment) { payment } }
context "when no default credit card is found" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
end
it "returns false and down not update the payment source" do
expect do
expect(payment_setup.__send__(:ensure_credit_card)).to be false
end.to_not change(payment, :source).from(nil)
end
end
context "when a default credit card is found" do
let(:credit_card) { create(:credit_card) }
before do
allow(order).to receive(:user) {
instance_double(Spree::User, default_card: credit_card)
}
end
context "and charge have not been authorised by the customer" do
before do
allow(order).to receive(:customer) {
instance_double(Customer, allow_charges?: false)
}
end
it "returns false and does not update the payment source" do
expect do
expect(payment_setup.__send__(:ensure_credit_card)).to be false
end.to_not change(payment, :source).from(nil)
end
end
context "and charges have been authorised by the customer" do
before do
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: true) }
end
it "returns true and stores the credit card as the payment source" do
expect do
expect(payment_setup.__send__(:ensure_credit_card)).to be true
end.to change(payment, :source_id).from(nil).to(credit_card.id)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,437 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe ProxyOrderSyncer do
describe "initialization" do
let!(:subscription) { create(:subscription) }
it "raises an error when initialized with an object
that is not a Subscription or an ActiveRecord::Relation" do
expect{ ProxyOrderSyncer.new(subscription) }.to_not raise_error
expect{ ProxyOrderSyncer.new(Subscription.where(id: subscription.id)) }.to_not raise_error
expect{ ProxyOrderSyncer.new("something") }.to raise_error RuntimeError
end
end
describe "#sync!" do
let(:now) { Time.zone.now }
let(:schedule) { create(:schedule) }
let(:closed_oc) { # Closed
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now - 1.minute,
orders_close_at: now)
}
let(:open_oc_closes_before_begins_at_oc) { # Open, but closes before begins at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now - 1.minute,
orders_close_at: now + 59.seconds)
}
let(:open_oc) { # Open & closes between begins at and ends at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now - 1.minute,
orders_close_at: now + 90.seconds)
}
let(:upcoming_closes_before_begins_at_oc) { # Upcoming, but closes before begins at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 59.seconds)
}
let(:upcoming_closes_on_begins_at_oc) { # Upcoming & closes on begins at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 1.minute)
}
let(:upcoming_closes_on_ends_at_oc) { # Upcoming & closes on ends at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 2.minutes)
}
let(:upcoming_closes_after_ends_at_oc) { # Upcoming & closes after ends at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 121.seconds)
}
let(:subscription) {
build(:subscription, schedule: schedule,
begins_at: now + 1.minute,
ends_at: now + 2.minutes)
}
let(:proxy_orders) { subscription.reload.proxy_orders }
let(:order_cycles) { proxy_orders.map(&:order_cycle) }
let(:syncer) { ProxyOrderSyncer.new(subscription) }
context "when the subscription is not persisted" do
before do
oc # Ensure oc is created before we attempt to sync
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
end
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule has an open OC that closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes upcoming oc that closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
end
context "when the subscription is persisted" do
before { expect(subscription.save!).to be true }
context "when a proxy order exists" do
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: oc) }
context "for an oc included in the relevant schedule" do
context "and the proxy order has already been placed" do
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
end
context "and the proxy order has not already been placed" do
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
end
end
context "for an oc not included in the relevant schedule" do
let!(:proxy_order) {
create(:proxy_order, subscription: subscription, order_cycle: open_oc)
}
before do
open_oc.schedule_ids = []
expect(open_oc.save!).to be true
end
context "and the proxy order has already been placed" do
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
end
context "and the proxy order has not already been placed" do
# This shouldn't really happen, but it is possible
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
# This shouldn't really happen, but it is possible
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
end
end
end
context "when a proxy order does not exist" do
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
let!(:oc) { closed_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule has an open oc that closes between begins_at and ends_at" do
let!(:oc) { open_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes before begins_at" do
let!(:oc) { upcoming_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes upcoming oc that closes on begins_at" do
let!(:oc) { upcoming_closes_on_begins_at_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes on ends_at" do
let!(:oc) { upcoming_closes_on_ends_at_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let!(:oc) { upcoming_closes_after_ends_at_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
end
end
end
end
end
end

View File

@@ -0,0 +1,132 @@
# frozen_string_literal: true
require 'spec_helper'
module OrderManagement
module Subscriptions
describe Summarizer do
let(:order) { create(:order) }
let(:summarizer) { OrderManagement::Subscriptions::Summarizer.new }
before { allow(Rails.logger).to receive(:info) }
describe "#summary_for" do
let(:order) { double(:order, distributor_id: 123) }
context "when a summary for the order's distributor doesn't already exist" do
it "initializes a new summary object, and returns it" do
expect(summarizer.instance_variable_get(:@summaries).count).to be 0
summary = summarizer.__send__(:summary_for, order)
expect(summary.shop_id).to be 123
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
end
end
context "when a summary for the order's distributor already exists" do
let(:summary) { double(:summary) }
before do
summarizer.instance_variable_set(:@summaries, 123 => summary)
end
it "returns the existing summary object" do
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
expect(summarizer.__send__(:summary_for, order)).to eq summary
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
end
end
end
describe "recording events" do
let(:order) { double(:order) }
let(:summary) { double(:summary) }
before { allow(summarizer).to receive(:summary_for).with(order) { summary } }
describe "#record_order" do
it "requests a summary for the order and calls #record_order on it" do
expect(summary).to receive(:record_order).with(order).once
summarizer.record_order(order)
end
end
describe "#record_success" do
it "requests a summary for the order and calls #record_success on it" do
expect(summary).to receive(:record_success).with(order).once
summarizer.record_success(order)
end
end
describe "#record_issue" do
it "requests a summary for the order and calls #record_issue on it" do
expect(order).to receive(:id)
expect(summary).to receive(:record_issue).with(:type, order, "message").once
summarizer.record_issue(:type, order, "message")
end
end
describe "#record_and_log_error" do
before do
allow(order).to receive(:number) { "123" }
end
context "when errors exist on the order" do
before do
allow(order).to receive(:errors) {
double(:errors, any?: true, full_messages: ["Some error"])
}
end
it "sends error info to rails logger and calls #record_issue with an error message" do
expect(summarizer).to receive(:record_issue).with(:processing,
order, "Errors: Some error")
summarizer.record_and_log_error(:processing, order)
end
end
context "when no errors exist on the order" do
before do
allow(order).to receive(:errors) { double(:errors, any?: false) }
end
it "falls back to calling record_issue" do
expect(summarizer).to receive(:record_issue).with(:processing, order)
summarizer.record_and_log_error(:processing, order)
end
end
end
end
describe "#send_placement_summary_emails" do
let(:summary1) { double(:summary) }
let(:summary2) { double(:summary) }
let(:summaries) { { 1 => summary1, 2 => summary2 } }
let(:mail_mock) { double(:mail, deliver: true) }
before do
summarizer.instance_variable_set(:@summaries, summaries)
end
it "sends a placement summary email for each summary" do
expect(SubscriptionMailer).to receive(:placement_summary_email).twice { mail_mock }
summarizer.send_placement_summary_emails
end
end
describe "#send_confirmation_summary_emails" do
let(:summary1) { double(:summary) }
let(:summary2) { double(:summary) }
let(:summaries) { { 1 => summary1, 2 => summary2 } }
let(:mail_mock) { double(:mail, deliver: true) }
before do
summarizer.instance_variable_set(:@summaries, summaries)
end
it "sends a placement summary email for each summary" do
expect(SubscriptionMailer).to receive(:confirmation_summary_email).twice { mail_mock }
summarizer.send_confirmation_summary_emails
end
end
end
end
end

View File

@@ -0,0 +1,127 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe Summary do
let(:summary) { OrderManagement::Subscriptions::Summary.new(123) }
describe "#initialize" do
it "initializes instance variables: shop_id, order_count, success_count and issues" do
expect(summary.shop_id).to be 123
expect(summary.order_count).to be 0
expect(summary.success_count).to be 0
expect(summary.issues).to be_a Hash
end
end
describe "#record_order" do
let(:order) { double(:order, id: 37) }
it "adds the order id to the order_ids array" do
summary.record_order(order)
expect(summary.instance_variable_get(:@order_ids)).to eq [order.id]
end
end
describe "#record_success" do
let(:order) { double(:order, id: 37) }
it "adds the order id to the success_ids array" do
summary.record_success(order)
expect(summary.instance_variable_get(:@success_ids)).to eq [order.id]
end
end
describe "#record_issue" do
let(:order) { double(:order, id: 1) }
context "when no issues of the same type have been recorded yet" do
it "adds a new type to the issues hash, and stores a new issue against it" do
summary.record_issue(:some_type, order, "message")
expect(summary.issues.keys).to include :some_type
expect(summary.issues[:some_type][order.id]).to eq "message"
end
end
context "when an issue of the same type has already been recorded" do
let(:existing_issue) { double(:existing_issue) }
before { summary.issues[:some_type] = [existing_issue] }
it "stores a new issue against the existing type" do
summary.record_issue(:some_type, order, "message")
expect(summary.issues[:some_type]).to include existing_issue
expect(summary.issues[:some_type][order.id]).to eq "message"
end
end
end
describe "#order_count" do
let(:order_ids) { [1, 2, 3, 4, 5, 6, 7] }
it "counts the number of items in the order_ids instance_variable" do
summary.instance_variable_set(:@order_ids, order_ids)
expect(summary.order_count).to be 7
end
end
describe "#success_count" do
let(:success_ids) { [1, 2, 3, 4, 5, 6, 7] }
it "counts the number of items in the success_ids instance_variable" do
summary.instance_variable_set(:@success_ids, success_ids)
expect(summary.success_count).to be 7
end
end
describe "#issue_count" do
let(:order_ids) { [1, 3, 5, 7, 9] }
let(:success_ids) { [1, 2, 3, 4, 5] }
it "counts the number of items in order_ids that are not in success_ids" do
summary.instance_variable_set(:@order_ids, order_ids)
summary.instance_variable_set(:@success_ids, success_ids)
expect(summary.issue_count).to be 2 # 7 & 9
end
end
describe "#orders_affected_by" do
let(:order1) { create(:order) }
let(:order2) { create(:order) }
before do
allow(summary).to receive(:unrecorded_ids) { [order1.id] }
allow(summary).to receive(:issues) { { failure: { order2.id => "A message" } } }
end
context "when the issue type is :other" do
let(:orders) { summary.orders_affected_by(:other) }
it "returns orders specified by unrecorded_ids" do
expect(orders).to include order1
expect(orders).to_not include order2
end
end
context "when the issue type is :other" do
let(:orders) { summary.orders_affected_by(:failure) }
it "returns orders specified by the relevant issue hash" do
expect(orders).to include order2
expect(orders).to_not include order1
end
end
end
describe "#unrecorded_ids" do
let(:issues) { { type: { 7 => "message", 8 => "message" } } }
before do
summary.instance_variable_set(:@order_ids, [1, 3, 5, 7, 9])
summary.instance_variable_set(:@success_ids, [1, 2, 3, 4, 5])
summary.instance_variable_set(:@issues, issues)
end
it "returns order_ids that are not marked as an issue or a success" do
expect(summary.unrecorded_ids).to eq [9]
end
end
end
end
end

View File

@@ -0,0 +1,524 @@
# 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 exist" 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 exist but all are 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 exist and some are 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 exist" 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 exist" 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,165 @@
# 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

@@ -1,95 +0,0 @@
module OpenFoodNetwork
class ProxyOrderSyncer
attr_reader :subscription
delegate :order_cycles, :proxy_orders, :begins_at, :ends_at, to: :subscription
def initialize(subscriptions)
case subscriptions
when Subscription
@subscription = subscriptions
when ActiveRecord::Relation
@subscriptions = subscriptions.not_ended.not_canceled
else
raise "ProxyOrderSyncer must be initialized with " \
"an instance of Subscription or ActiveRecord::Relation"
end
end
def sync!
return sync_subscriptions! if @subscriptions
return initialise_proxy_orders! unless @subscription.id
sync_subscription!
end
private
def sync_subscriptions!
@subscriptions.each do |subscription|
@subscription = subscription
sync_subscription!
end
end
def initialise_proxy_orders!
uninitialised_order_cycle_ids.each do |order_cycle_id|
Rails.logger.info "Initializing Proxy Order " \
"of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
end
end
def sync_subscription!
Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
create_proxy_orders!
remove_orphaned_proxy_orders!
end
def create_proxy_orders!
return unless not_closed_in_range_order_cycles.any?
query = "INSERT INTO proxy_orders (subscription_id, order_cycle_id, updated_at, created_at)"
query << " VALUES #{insert_values}"
query << " ON CONFLICT DO NOTHING"
ActiveRecord::Base.connection.exec_query(query)
end
def uninitialised_order_cycle_ids
not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id)
end
def remove_orphaned_proxy_orders!
orphaned_proxy_orders.scoped.delete_all
end
# Remove Proxy Orders that have not been placed yet
# and are in Order Cycles that are out of range
def orphaned_proxy_orders
orphaned = proxy_orders.where(placed_at: nil)
order_cycle_ids = in_range_order_cycles.pluck(:id)
return orphaned unless order_cycle_ids.any?
orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids)
end
def insert_values
now = Time.now.utc.iso8601
not_closed_in_range_order_cycles
.map{ |oc| "(#{subscription.id},#{oc.id},'#{now}','#{now}')" }
.join(",")
end
def not_closed_in_range_order_cycles
in_range_order_cycles.merge(OrderCycle.not_closed)
end
def in_range_order_cycles
order_cycles.where("orders_close_at >= ? AND orders_close_at <= ?",
begins_at,
ends_at || 100.years.from_now)
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,65 +0,0 @@
module OpenFoodNetwork
class SubscriptionPaymentUpdater
def initialize(order)
@order = order
end
def update!
create_payment
ensure_payment_source
return if order.errors.any?
payment.update_attributes(amount: order.outstanding_balance)
end
private
attr_reader :order
def payment
@payment ||= order.pending_payments.last
end
def create_payment
return if payment.present?
@payment = order.payments.create(
payment_method_id: order.subscription.payment_method_id,
amount: order.outstanding_balance
)
end
def card_required?
[Spree::Gateway::StripeConnect,
Spree::Gateway::StripeSCA].include? payment.payment_method.class
end
def card_set?
payment.source is_a? Spree::CreditCard
end
def ensure_payment_source
return unless card_required? && !card_set?
ensure_credit_card || order.errors.add(:base, :no_card)
end
def ensure_credit_card
return false if saved_credit_card.blank? || !allow_charges?
payment.update_attributes(source: saved_credit_card)
end
def allow_charges?
order.customer.allow_charges?
end
def saved_credit_card
order.user.default_card
end
def errors_present?
order.errors.any?
end
end
end

View File

@@ -1,53 +0,0 @@
require 'open_food_network/subscription_summary'
# Used by for SubscriptionPlacementJob and SubscriptionConfirmJob to summarize the
# result of automatic processing of subscriptions for the relevant shop owners.
module OpenFoodNetwork
class SubscriptionSummarizer
def initialize
@summaries = {}
end
def record_order(order)
summary_for(order).record_order(order)
end
def record_success(order)
summary_for(order).record_success(order)
end
def record_issue(type, order, message = nil)
Rails.logger.info "Issue in Subscription Order #{order.id}: #{type}"
summary_for(order).record_issue(type, order, message)
end
def record_and_log_error(type, order)
return record_issue(type, order) unless order.errors.any?
error = "Subscription#{type.to_s.camelize}Error"
line1 = "#{error}: Cannot process order #{order.number} due to errors"
line2 = "Errors: #{order.errors.full_messages.join(', ')}"
Rails.logger.info("#{line1}\n#{line2}")
record_issue(type, order, line2)
end
def send_placement_summary_emails
@summaries.values.each do |summary|
SubscriptionMailer.placement_summary_email(summary).deliver
end
end
def send_confirmation_summary_emails
@summaries.values.each do |summary|
SubscriptionMailer.confirmation_summary_email(summary).deliver
end
end
private
def summary_for(order)
shop_id = order.distributor_id
@summaries[shop_id] ||= SubscriptionSummary.new(shop_id)
end
end
end

View File

@@ -1,49 +0,0 @@
module OpenFoodNetwork
class SubscriptionSummary
attr_reader :shop_id, :order_count, :success_count, :issues
def initialize(shop_id)
@shop_id = shop_id
@order_ids = []
@success_ids = []
@issues = {}
end
def record_order(order)
@order_ids << order.id
end
def record_success(order)
@success_ids << order.id
end
def record_issue(type, order, message)
issues[type] ||= {}
issues[type][order.id] = message
end
def order_count
@order_ids.count
end
def success_count
@success_ids.count
end
def issue_count
(@order_ids - @success_ids).count
end
def orders_affected_by(type)
case type
when :other then Spree::Order.where(id: unrecorded_ids)
else Spree::Order.where(id: issues[type].keys)
end
end
def unrecorded_ids
recorded_ids = issues.values.map(&:keys).flatten
@order_ids - @success_ids - recorded_ids
end
end
end

View File

@@ -90,7 +90,7 @@ describe Admin::SchedulesController, type: :controller do
it "syncs proxy orders when order_cycle_ids change" do
syncer_mock = double(:syncer)
allow(OpenFoodNetwork::ProxyOrderSyncer).to receive(:new) { syncer_mock }
allow(OrderManagement::Subscriptions::ProxyOrderSyncer).to receive(:new) { syncer_mock }
expect(syncer_mock).to receive(:sync!).exactly(2).times
spree_put :update, format: :json, id: coordinated_schedule.id, schedule: { order_cycle_ids: [coordinated_order_cycle.id, coordinated_order_cycle2.id] }
@@ -150,7 +150,7 @@ describe Admin::SchedulesController, type: :controller do
it "sync proxy orders" do
syncer_mock = double(:syncer)
allow(OpenFoodNetwork::ProxyOrderSyncer).to receive(:new) { syncer_mock }
allow(OrderManagement::Subscriptions::ProxyOrderSyncer).to receive(:new) { syncer_mock }
expect(syncer_mock).to receive(:sync!).once
create_schedule params

View File

@@ -16,7 +16,7 @@ describe SubscriptionConfirmJob do
placed_at: 5.minutes.ago)
end
let!(:order) { proxy_order.initialise_order! }
let(:proxy_orders) { job.send(:proxy_orders) }
let(:proxy_orders) { job.send(:unconfirmed_proxy_orders) }
before do
AdvanceOrderService.new(order).call!
@@ -80,8 +80,8 @@ describe SubscriptionConfirmJob do
before do
proxy_order.initialise_order!
allow(job).to receive(:proxy_orders) { ProxyOrder.where(id: proxy_order.id) }
allow(job).to receive(:process!)
allow(job).to receive(:unconfirmed_proxy_orders) { ProxyOrder.where(id: proxy_order.id) }
allow(job).to receive(:confirm_order!)
allow(job).to receive(:send_confirmation_summary_emails)
end
@@ -92,8 +92,7 @@ describe SubscriptionConfirmJob do
it "processes confirmable proxy_orders" do
job.perform
expect(job).to have_received(:process!)
expect(job.instance_variable_get(:@order)).to eq proxy_order.reload.order
expect(job).to have_received(:confirm_order!).with(proxy_order.reload.order)
end
it "sends a summary email" do
@@ -117,7 +116,7 @@ describe SubscriptionConfirmJob do
end
end
describe "processing an order" do
describe "confirming an order" do
let(:shop) { create(:distributor_enterprise) }
let(:order_cycle1) { create(:simple_order_cycle, coordinator: shop) }
let(:order_cycle2) { create(:simple_order_cycle, coordinator: shop) }
@@ -128,35 +127,34 @@ describe SubscriptionConfirmJob do
before do
AdvanceOrderService.new(order).call!
allow(job).to receive(:send_confirm_email).and_call_original
job.instance_variable_set(:@order, order)
allow(job).to receive(:send_confirmation_email).and_call_original
setup_email
expect(job).to receive(:record_order).with(order)
expect(job).to receive(:record_order)
end
context "when payments need to be processed" do
let(:payment_method) { create(:payment_method) }
let(:payment) { double(:payment, amount: 10) }
let(:payment) { create(:payment, amount: 10) }
before do
allow(order).to receive(:payment_total) { 0 }
allow(order).to receive(:total) { 10 }
allow(order).to receive(:payment_required?) { true }
allow(order).to receive(:pending_payments) { [payment] }
end
context "and an error is added to the order when updating payments" do
before { expect(job).to receive(:update_payment!) { order.errors.add(:base, "a payment error") } }
before do
expect(job).to receive(:setup_payment!) { |order| order.errors.add(:base, "a payment error") }
end
it "sends a failed payment email" do
expect(job).to receive(:send_failed_payment_email)
expect(job).to_not receive(:send_confirm_email)
job.send(:process!)
expect(job).to_not receive(:send_confirmation_email)
job.send(:confirm_order!, order)
end
end
context "and no errors are added when updating payments" do
before { expect(job).to receive(:update_payment!) { true } }
before { expect(job).to receive(:setup_payment!) { true } }
context "when an error occurs while processing the payment" do
before do
@@ -165,8 +163,8 @@ describe SubscriptionConfirmJob do
it "sends a failed payment email" do
expect(job).to receive(:send_failed_payment_email)
expect(job).to_not receive(:send_confirm_email)
job.send(:process!)
expect(job).to_not receive(:send_confirmation_email)
job.send(:confirm_order!, order)
end
end
@@ -182,8 +180,8 @@ describe SubscriptionConfirmJob do
it "sends only a subscription confirm email, no regular confirmation emails" do
ActionMailer::Base.deliveries.clear
expect{ job.send(:process!) }.to_not enqueue_job ConfirmOrderJob
expect(job).to have_received(:send_confirm_email).once
expect{ job.send(:confirm_order!, order) }.to_not enqueue_job ConfirmOrderJob
expect(job).to have_received(:send_confirmation_email).once
expect(ActionMailer::Base.deliveries.count).to be 1
end
end
@@ -191,19 +189,18 @@ describe SubscriptionConfirmJob do
end
end
describe "#send_confirm_email" do
describe "#send_confirmation_email" do
let(:order) { instance_double(Spree::Order) }
let(:mail_mock) { double(:mailer_mock, deliver: true) }
before do
job.instance_variable_set(:@order, order)
allow(SubscriptionMailer).to receive(:confirmation_email) { mail_mock }
end
it "records a success and sends the email" do
expect(order).to receive(:update!)
expect(job).to receive(:record_success).with(order).once
job.send(:send_confirm_email)
job.send(:send_confirmation_email, order)
expect(SubscriptionMailer).to have_received(:confirmation_email).with(order)
expect(mail_mock).to have_received(:deliver)
end
@@ -214,14 +211,13 @@ describe SubscriptionConfirmJob do
let(:mail_mock) { double(:mailer_mock, deliver: true) }
before do
job.instance_variable_set(:@order, order)
allow(SubscriptionMailer).to receive(:failed_payment_email) { mail_mock }
end
it "records and logs an error and sends the email" do
expect(order).to receive(:update!)
expect(job).to receive(:record_and_log_error).with(:failed_payment, order).once
job.send(:send_failed_payment_email)
job.send(:send_failed_payment_email, order)
expect(SubscriptionMailer).to have_received(:failed_payment_email).with(order)
expect(mail_mock).to have_received(:deliver)
end

View File

@@ -1,400 +0,0 @@
require 'open_food_network/proxy_order_syncer'
module OpenFoodNetwork
describe ProxyOrderSyncer do
describe "initialization" do
let!(:subscription) { create(:subscription) }
it "raises an error when initialized with an object that is not a Subscription or an ActiveRecord::Relation" do
expect{ ProxyOrderSyncer.new(subscription) }.to_not raise_error
expect{ ProxyOrderSyncer.new(Subscription.where(id: subscription.id)) }.to_not raise_error
expect{ ProxyOrderSyncer.new("something") }.to raise_error RuntimeError
end
end
describe "#sync!" do
let(:now) { Time.zone.now }
let(:schedule) { create(:schedule) }
let(:closed_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now) } # Closed
let(:open_oc_closes_before_begins_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 59.seconds) } # Open, but closes before begins at
let(:open_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 90.seconds) } # Open & closes between begins at and ends at
let(:upcoming_closes_before_begins_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 59.seconds) } # Upcoming, but closes before begins at
let(:upcoming_closes_on_begins_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 1.minute) } # Upcoming & closes on begins at
let(:upcoming_closes_on_ends_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 2.minutes) } # Upcoming & closes on ends at
let(:upcoming_closes_after_ends_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 121.seconds) } # Upcoming & closes after ends at
let(:subscription) { build(:subscription, schedule: schedule, begins_at: now + 1.minute, ends_at: now + 2.minutes) }
let(:proxy_orders) { subscription.reload.proxy_orders }
let(:order_cycles) { proxy_orders.map(&:order_cycle) }
let(:syncer) { ProxyOrderSyncer.new(subscription) }
context "when the subscription is not persisted" do
before do
oc # Ensure oc is created before we attempt to sync
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
end
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes upcoming oc that closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
end
context "when the subscription is persisted" do
before { expect(subscription.save!).to be true }
context "when a proxy order exists" do
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: oc) }
context "for an oc included in the relevant schedule" do
context "and the proxy order has already been placed" do
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
end
context "and the proxy order has not already been placed" do
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
end
end
context "for an oc not included in the relevant schedule" do
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: open_oc) }
before do
open_oc.schedule_ids = []
expect(open_oc.save!).to be true
end
context "and the proxy order has already been placed" do
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
end
context "and the proxy order has not already been placed" do
# This shouldn't really happen, but it is possible
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
# This shouldn't really happen, but it is possible
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
end
end
end
context "when a proxy order does not exist" do
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
let!(:oc) { closed_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes between begins_at and ends_at" do
let!(:oc) { open_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes before begins_at" do
let!(:oc) { upcoming_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes upcoming oc that closes on begins_at" do
let!(:oc) { upcoming_closes_on_begins_at_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes on ends_at" do
let!(:oc) { upcoming_closes_on_ends_at_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let!(:oc) { upcoming_closes_after_ends_at_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
end
end
end
end
end

View File

@@ -1,216 +0,0 @@
require 'spec_helper'
require 'open_food_network/subscription_payment_updater'
module OpenFoodNetwork
describe SubscriptionPaymentUpdater do
let(:order) { create(:order) }
let(:updater) { OpenFoodNetwork::SubscriptionPaymentUpdater.new(order) }
describe "#payment" do
context "when only one payment exists on the order" do
let!(:payment) { create(:payment, order: order) }
context "where the payment is pending" do
it { expect(updater.send(:payment)).to eq payment }
end
context "where the payment is failed" do
before { payment.update_attribute(:state, 'failed') }
it { expect(updater.send(:payment)).to be nil }
end
end
context "when more that one payment exists on the order" do
let!(:payment1) { create(:payment, order: order) }
let!(:payment2) { create(:payment, order: order) }
context "where more than one payment is pending" do
it { expect([payment1, payment2]).to include updater.send(:payment) }
end
context "where only one payment is pending" do
before { payment1.update_attribute(:state, 'failed') }
it { expect(updater.send(:payment)).to eq payment2 }
end
context "where no payments are pending" do
before do
payment1.update_attribute(:state, 'failed')
payment2.update_attribute(:state, 'failed')
end
it { expect(updater.send(:payment)).to be nil }
end
end
end
describe "#update!" do
let!(:payment){ create(:payment, amount: 10) }
context "when no pending payments are present" do
let(:payment_method) { create(:payment_method) }
let(:subscription) { double(:subscription, payment_method_id: payment_method.id) }
before do
allow(order).to receive(:pending_payments).once { [] }
allow(order).to receive(:outstanding_balance) { 5 }
allow(order).to receive(:subscription) { subscription }
end
it "creates a new payment on the order" do
expect{ updater.update! }.to change(Spree::Payment, :count).by(1)
expect(order.payments.first.amount).to eq 5
end
end
context "when a payment is present" do
before { allow(order).to receive(:pending_payments).once { [payment] } }
context "when a credit card is not required" do
before do
allow(updater).to receive(:card_required?) { false }
expect(updater).to_not receive(:card_available?)
expect(updater).to_not receive(:ensure_credit_card)
end
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ updater.update! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ updater.update! }.to_not change(payment, :amount).from(10)
end
end
end
context "when a credit card is required" do
before do
expect(updater).to receive(:card_required?) { true }
end
context "and the payment source is not a credit card" do
before { expect(updater).to receive(:card_set?) { false } }
context "and no default credit card has been set by the customer" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
end
it "adds an error to the order and does not update the payment" do
expect(payment).to_not receive(:update_attributes)
expect{ updater.update! }.to change(order.errors[:base], :count).from(0).to(1)
end
end
context "and the customer has not authorised the shop to charge to credit cards" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: create(:credit_card)) }
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: false) }
end
it "adds an error to the order and does not update the payment" do
expect(payment).to_not receive(:update_attributes)
expect{ updater.update! }.to change(order.errors[:base], :count).from(0).to(1)
end
end
context "and an authorised default credit card is available to charge" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: create(:credit_card)) }
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: true) }
end
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ updater.update! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ updater.update! }.to_not change(payment, :amount).from(10)
end
end
end
end
context "and the payment source is already a credit card" do
before { expect(updater).to receive(:card_set?) { true } }
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ updater.update! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ updater.update! }.to_not change(payment, :amount).from(10)
end
end
end
end
end
end
describe "#ensure_credit_card" do
let!(:payment) { create(:payment, source: nil) }
before { allow(updater).to receive(:payment) { payment } }
context "when no default credit card is found" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
end
it "returns false and down not update the payment source" do
expect do
expect(updater.send(:ensure_credit_card)).to be false
end.to_not change(payment, :source).from(nil)
end
end
context "when a default credit card is found" do
let(:credit_card) { create(:credit_card) }
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: credit_card) }
end
context "and charge have not been authorised by the customer" do
before do
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: false) }
end
it "returns false and does not update the payment source" do
expect do
expect(updater.send(:ensure_credit_card)).to be false
end.to_not change(payment, :source).from(nil)
end
end
context "and charges have been authorised by the customer" do
before do
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: true) }
end
it "returns true and stores the credit card as the payment source" do
expect do
expect(updater.send(:ensure_credit_card)).to be true
end.to change(payment, :source_id).from(nil).to(credit_card.id)
end
end
end
end
end
end

View File

@@ -1,126 +0,0 @@
require 'spec_helper'
require 'open_food_network/subscription_summarizer'
module OpenFoodNetwork
describe SubscriptionSummarizer do
let(:order) { create(:order) }
let(:summarizer) { OpenFoodNetwork::SubscriptionSummarizer.new }
before { allow(Rails.logger).to receive(:info) }
describe "#summary_for" do
let(:order) { double(:order, distributor_id: 123) }
context "when a summary for the order's distributor doesn't already exist" do
it "initializes a new summary object, and returns it" do
expect(summarizer.instance_variable_get(:@summaries).count).to be 0
summary = summarizer.send(:summary_for, order)
expect(summary.shop_id).to be 123
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
end
end
context "when a summary for the order's distributor already exists" do
let(:summary) { double(:summary) }
before do
summarizer.instance_variable_set(:@summaries, 123 => summary)
end
it "returns the existing summary object" do
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
expect(summarizer.send(:summary_for, order)).to eq summary
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
end
end
end
describe "recording events" do
let(:order) { double(:order) }
let(:summary) { double(:summary) }
before { allow(summarizer).to receive(:summary_for).with(order) { summary } }
describe "#record_order" do
it "requests a summary for the order and calls #record_order on it" do
expect(summary).to receive(:record_order).with(order).once
summarizer.record_order(order)
end
end
describe "#record_success" do
it "requests a summary for the order and calls #record_success on it" do
expect(summary).to receive(:record_success).with(order).once
summarizer.record_success(order)
end
end
describe "#record_issue" do
it "requests a summary for the order and calls #record_issue on it" do
expect(order).to receive(:id)
expect(summary).to receive(:record_issue).with(:type, order, "message").once
summarizer.record_issue(:type, order, "message")
end
end
describe "#record_and_log_error" do
before do
allow(order).to receive(:number) { "123" }
end
context "when errors exist on the order" do
before do
allow(order).to receive(:errors) { double(:errors, any?: true, full_messages: ["Some error"]) }
end
it "sends error info to the rails logger and calls #record_issue on itself with an error message" do
expect(summarizer).to receive(:record_issue).with(:processing, order, "Errors: Some error")
summarizer.record_and_log_error(:processing, order)
end
end
context "when no errors exist on the order" do
before do
allow(order).to receive(:errors) { double(:errors, any?: false) }
end
it "falls back to calling record_issue" do
expect(summarizer).to receive(:record_issue).with(:processing, order)
summarizer.record_and_log_error(:processing, order)
end
end
end
end
describe "#send_placement_summary_emails" do
let(:summary1) { double(:summary) }
let(:summary2) { double(:summary) }
let(:summaries) { { 1 => summary1, 2 => summary2 } }
let(:mail_mock) { double(:mail, deliver: true) }
before do
summarizer.instance_variable_set(:@summaries, summaries)
end
it "sends a placement summary email for each summary" do
expect(SubscriptionMailer).to receive(:placement_summary_email).twice { mail_mock }
summarizer.send_placement_summary_emails
end
end
describe "#send_confirmation_summary_emails" do
let(:summary1) { double(:summary) }
let(:summary2) { double(:summary) }
let(:summaries) { { 1 => summary1, 2 => summary2 } }
let(:mail_mock) { double(:mail, deliver: true) }
before do
summarizer.instance_variable_set(:@summaries, summaries)
end
it "sends a placement summary email for each summary" do
expect(SubscriptionMailer).to receive(:confirmation_summary_email).twice { mail_mock }
summarizer.send_confirmation_summary_emails
end
end
end
end

View File

@@ -1,125 +0,0 @@
require 'open_food_network/subscription_summary'
module OpenFoodNetwork
describe SubscriptionSummary do
let(:summary) { OpenFoodNetwork::SubscriptionSummary.new(123) }
describe "#initialize" do
it "initializes instance variables: shop_id, order_count, success_count and issues" do
expect(summary.shop_id).to be 123
expect(summary.order_count).to be 0
expect(summary.success_count).to be 0
expect(summary.issues).to be_a Hash
end
end
describe "#record_order" do
let(:order) { double(:order, id: 37) }
it "adds the order id to the order_ids array" do
summary.record_order(order)
expect(summary.instance_variable_get(:@order_ids)).to eq [order.id]
end
end
describe "#record_success" do
let(:order) { double(:order, id: 37) }
it "adds the order id to the success_ids array" do
summary.record_success(order)
expect(summary.instance_variable_get(:@success_ids)).to eq [order.id]
end
end
describe "#record_issue" do
let(:order) { double(:order, id: 1) }
context "when no issues of the same type have been recorded yet" do
it "adds a new type to the issues hash, and stores a new issue against it" do
summary.record_issue(:some_type, order, "message")
expect(summary.issues.keys).to include :some_type
expect(summary.issues[:some_type][order.id]).to eq "message"
end
end
context "when an issue of the same type has already been recorded" do
let(:existing_issue) { double(:existing_issue) }
before { summary.issues[:some_type] = [existing_issue] }
it "stores a new issue against the existing type" do
summary.record_issue(:some_type, order, "message")
expect(summary.issues[:some_type]).to include existing_issue
expect(summary.issues[:some_type][order.id]).to eq "message"
end
end
end
describe "#order_count" do
let(:order_ids) { [1, 2, 3, 4, 5, 6, 7] }
it "counts the number of items in the order_ids instance_variable" do
summary.instance_variable_set(:@order_ids, order_ids)
expect(summary.order_count).to be 7
end
end
describe "#success_count" do
let(:success_ids) { [1, 2, 3, 4, 5, 6, 7] }
it "counts the number of items in the success_ids instance_variable" do
summary.instance_variable_set(:@success_ids, success_ids)
expect(summary.success_count).to be 7
end
end
describe "#issue_count" do
let(:order_ids) { [1, 3, 5, 7, 9] }
let(:success_ids) { [1, 2, 3, 4, 5] }
it "counts the number of items in order_ids that are not in success_ids" do
summary.instance_variable_set(:@order_ids, order_ids)
summary.instance_variable_set(:@success_ids, success_ids)
expect(summary.issue_count).to be 2 # 7 & 9
end
end
describe "#orders_affected_by" do
let(:order1) { create(:order) }
let(:order2) { create(:order) }
before do
allow(summary).to receive(:unrecorded_ids) { [order1.id] }
allow(summary).to receive(:issues) { { failure: { order2.id => "A message" } } }
end
context "when the issue type is :other" do
let(:orders) { summary.orders_affected_by(:other) }
it "returns orders specified by unrecorded_ids" do
expect(orders).to include order1
expect(orders).to_not include order2
end
end
context "when the issue type is :other" do
let(:orders) { summary.orders_affected_by(:failure) }
it "returns orders specified by the relevant issue hash" do
expect(orders).to include order2
expect(orders).to_not include order1
end
end
end
describe "#unrecorded_ids" do
let(:issues) { { type: { 7 => "message", 8 => "message" } } }
before do
summary.instance_variable_set(:@order_ids, [1, 3, 5, 7, 9])
summary.instance_variable_set(:@success_ids, [1, 2, 3, 4, 5])
summary.instance_variable_set(:@issues, issues)
end
it "returns order_ids that are not marked as an issue or a success" do
expect(summary.unrecorded_ids).to eq [9]
end
end
end
end

View File

@@ -1,67 +0,0 @@
require 'open_food_network/proxy_order_syncer'
module OpenFoodNetwork
describe ProxyOrderSyncer, performance: true do
let(:start) { Time.zone.now.beginning_of_day }
let!(:schedule) { create(:schedule, order_cycles: order_cycles) }
let!(:order_cycles) do
Array.new(10) do |i|
create(:simple_order_cycle, orders_open_at: start + i.days,
orders_close_at: start + (i + 1).days )
end
end
let!(:subscriptions) do
Array.new(150) do |_i|
create(:subscription, schedule: schedule, begins_at: start, ends_at: start + 10.days)
end
Subscription.where(schedule_id: schedule)
end
context "measuring performance for initialisation" do
it "reports the average run time for adding 10 OCs to 150 subscriptions" do
expect(ProxyOrder.count).to be 0
times = []
10.times do
syncer = ProxyOrderSyncer.new(subscriptions.reload)
t1 = Time.zone.now
syncer.sync!
t2 = Time.zone.now
diff = t2 - t1
times << diff
puts diff.round(2)
expect(ProxyOrder.count).to be 1500
ProxyOrder.destroy_all
end
puts "AVG: #{(times.sum / times.count).round(2)}"
end
end
context "measuring performance for removal" do
it "reports the average run time for removing 8 OCs from 150 subscriptions" do
times = []
10.times do
syncer = ProxyOrderSyncer.new(subscriptions.reload)
syncer.sync!
expect(ProxyOrder.count).to be 1500
subscriptions.update_all(begins_at: start + 8.days + 1.minute)
syncer = ProxyOrderSyncer.new(subscriptions.reload)
t1 = Time.zone.now
syncer.sync!
t2 = Time.zone.now
diff = t2 - t1
times << diff
puts diff.round(2)
expect(ProxyOrder.count).to be 300
subscriptions.update_all(begins_at: start)
end
puts "AVG: #{(times.sum / times.count).round(2)}"
end
end
end
end

View File

@@ -1,3 +1,5 @@
require 'order_management/subscriptions/proxy_order_syncer'
describe OrderCycleForm do
describe "save" do
describe "creating a new order cycle from params" do
@@ -66,10 +68,10 @@ describe OrderCycleForm do
context "where I manage the order_cycle's coordinator" do
let(:form) { OrderCycleForm.new(coordinated_order_cycle, params, user) }
let(:syncer_mock) { instance_double(OpenFoodNetwork::ProxyOrderSyncer, sync!: true) }
let(:syncer_mock) { instance_double(OrderManagement::Subscriptions::ProxyOrderSyncer, sync!: true) }
before do
allow(OpenFoodNetwork::ProxyOrderSyncer).to receive(:new) { syncer_mock }
allow(OrderManagement::Subscriptions::ProxyOrderSyncer).to receive(:new) { syncer_mock }
end
context "and I add an schedule that I own, and remove another that I own" do

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