Extract validation logic for standing orders into service object

This commit is contained in:
Rob Harrington
2018-01-17 13:31:05 +11:00
parent 0c302b022a
commit 2c40252edb
4 changed files with 584 additions and 101 deletions

View File

@@ -1,11 +1,7 @@
require 'open_food_network/proxy_order_syncer'
class StandingOrderForm
include ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_accessor :standing_order, :params, :fee_calculator, :order_update_issues
attr_accessor :standing_order, :params, :fee_calculator, :order_update_issues, :validator
delegate :orders, :order_cycles, :bill_address, :ship_address, :standing_line_items, to: :standing_order
delegate :shop, :shop_id, :customer, :customer_id, :begins_at, :ends_at, :proxy_orders, to: :standing_order
@@ -14,22 +10,14 @@ class StandingOrderForm
delegate :shipping_method_id_changed?, :shipping_method_id_was, :payment_method_id_changed?, :payment_method_id_was, to: :standing_order
delegate :credit_card_id, :credit_card, to: :standing_order
validates_presence_of :shop, :customer, :schedule, :payment_method, :shipping_method
validates_presence_of :bill_address, :ship_address, :begins_at
validate :ends_at_after_begins_at?
validate :customer_allowed?
validate :schedule_allowed?
validate :payment_method_allowed?
validate :shipping_method_allowed?
validate :standing_line_items_present?
validate :standing_line_items_available?
validate :credit_card_ok?
delegate :json_errors, :valid?, to: :validator
def initialize(standing_order, params = {}, fee_calculator = nil)
@standing_order = standing_order
@params = params
@fee_calculator = fee_calculator
@order_update_issues = {}
@validator = StandingOrderValidator.new(standing_order)
end
def save
@@ -43,12 +31,6 @@ class StandingOrderForm
end
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 update_initialised_orders
@@ -190,77 +172,4 @@ class StandingOrderForm
errors.add(k, msg)
end
end
def ends_at_after_begins_at?
# Does not add error even if ends_at is nil
# Note: presence of begins_at validated on the model
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 payment_method_allowed?
return unless payment_method
if payment_method.distributors.exclude?(shop)
errors.add(:payment_method, :not_available_to_shop, shop: shop.name)
end
return if StandingOrder::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type
errors.add(:payment_method, :invalid_type)
end
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 standing_line_items_present?
return if standing_line_items.reject(&:marked_for_destruction?).any?
errors.add(:standing_line_items, :at_least_one_product)
end
def standing_line_items_available?
available_variant_ids = variant_ids_for_shop_and_schedule
standing_line_items.each do |sli|
unless available_variant_ids.include? sli.variant_id
name = "#{sli.variant.product.name} - #{sli.variant.full_name}"
errors.add(:standing_line_items, :not_available, name: name)
end
end
end
def credit_card_ok?
return unless payment_method.andand.type == "Spree::Gateway::StripeConnect"
return errors.add(:credit_card, :blank) unless credit_card_id
return if customer.andand.user.andand.credit_card_ids.andand.include? credit_card_id
errors.add(:credit_card, :not_available)
end
def variant_ids_for_shop_and_schedule
Spree::Variant.joins(exchanges: { order_cycle: :schedules})
.where(id: standing_line_items.map(&:variant_id))
.where(schedules: { id: schedule}, exchanges: { incoming: false, receiver_id: shop })
.merge(OrderCycle.not_closed)
.select('DISTINCT spree_variants.id')
.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

@@ -0,0 +1,115 @@
# Encapsulation of all of the validation logic required for standing orders
# Public interface consists of #valid? method provided by ActiveModel::Validations
# and #json_errors which compiles a serializable hash of errors
class StandingOrderValidator
include ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :standing_order
validates_presence_of :shop, :customer, :schedule, :shipping_method, :payment_method
validates_presence_of :bill_address, :ship_address, :begins_at
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 :standing_line_items_present?
validate :requested_variants_available?
delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :standing_order
delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :standing_order
delegate :credit_card, :credit_card_id, to: :standing_order
delegate :standing_line_items, to: :standing_order
def initialize(standing_order)
@standing_order = standing_order
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 StandingOrder::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 payment_method.andand.type == "Spree::Gateway::StripeConnect"
return errors.add(:credit_card, :blank) unless credit_card_id
return if customer.andand.user.andand.credit_card_ids.andand.include? credit_card_id
errors.add(:credit_card, :not_available)
end
def standing_line_items_present?
return if standing_line_items.reject(&:marked_for_destruction?).any?
errors.add(:standing_line_items, :at_least_one_product)
end
def requested_variants_available?
standing_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(:standing_line_items, :not_available, name: name)
end
# TODO: Extract this into a separate class
def available_variant_ids
@available_variant_ids ||=
Spree::Variant.joins(exchanges: { order_cycle: :schedules })
.where(id: standing_line_items.map(&:variant_id))
.where(schedules: { id: schedule}, exchanges: { incoming: false, receiver_id: shop })
.merge(OrderCycle.not_closed)
.select('DISTINCT spree_variants.id')
.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,6 +1,4 @@
describe StandingOrderForm do
let(:error_t_scope) { 'activemodel.errors.models.standing_order_form.attributes' }
describe "creating a new standing order" do
let!(:shop) { create(:distributor_enterprise) }
let!(:customer) { create(:customer, enterprise: shop) }
@@ -225,7 +223,6 @@ describe StandingOrderForm do
expect(payments.with_state('void').count).to be 0
expect(payments.with_state('checkout').count).to be 1
expect(payments.with_state('checkout').first.payment_method).to eq payment_method
expect(form.errors[:credit_card]).to include I18n.t("#{error_t_scope}.credit_card.blank")
end
end
end
@@ -241,7 +238,6 @@ describe StandingOrderForm do
expect(payments.with_state('void').count).to be 0
expect(payments.with_state('checkout').count).to be 1
expect(payments.with_state('checkout').first.payment_method).to eq payment_method
expect(form.errors[:payment_method]).to include I18n.t("#{error_t_scope}.payment_method.invalid_type")
end
end
end
@@ -257,7 +253,6 @@ describe StandingOrderForm do
expect(payments.with_state('void').count).to be 0
expect(payments.with_state('checkout').count).to be 1
expect(payments.with_state('checkout').first.payment_method).to eq payment_method
expect(form.errors[:payment_method]).to include I18n.t("#{error_t_scope}.payment_method.not_available_to_shop", shop: standing_order.shop.name)
end
end
end
@@ -603,7 +598,6 @@ describe StandingOrderForm do
line_items = Spree::LineItem.where(order_id: standing_order.orders, variant_id: variant.id)
expect(line_items.count).to be 0
expect(order.reload.total.to_f).to eq 59.97
expect(form.json_errors.keys).to eq [:standing_line_items]
end
end
end
@@ -640,7 +634,6 @@ describe StandingOrderForm do
line_items = Spree::LineItem.where(order_id: standing_order.orders, variant_id: variant.id)
expect(line_items.count).to be 1
expect(order.reload.total.to_f).to eq 19.99
expect(form.json_errors.keys).to eq [:standing_line_items]
end
end
end

View File

@@ -0,0 +1,466 @@
describe StandingOrderValidator do
let(:shop) { instance_double(Enterprise, name: "Shop") }
describe "delegation" do
let(:standing_order) { create(:standing_order) }
let(:validator) { StandingOrderValidator.new(standing_order) }
it "delegates to standing_order" do
expect(validator.shop).to eq standing_order.shop
expect(validator.customer).to eq standing_order.customer
expect(validator.schedule).to eq standing_order.schedule
expect(validator.shipping_method).to eq standing_order.shipping_method
expect(validator.payment_method).to eq standing_order.payment_method
expect(validator.bill_address).to eq standing_order.bill_address
expect(validator.ship_address).to eq standing_order.ship_address
expect(validator.begins_at).to eq standing_order.begins_at
expect(validator.ends_at).to eq standing_order.ends_at
end
end
describe "validations" do
let(:standing_order_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,
credit_card: 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,
standing_line_items_present?: true,
requested_variants_available?: true
}
end
let(:standing_order) { instance_double(StandingOrder, standing_order_stubs) }
let(:validator) { StandingOrderValidator.new(standing_order) }
def stub_validations(validator, methods)
methods.each do |name, value|
allow(validator).to receive(name) { value }
end
end
describe "shipping method validation" do
let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:shipping_method)) }
before { stub_validations(validator, validation_stubs.except(:shipping_method_allowed?)) }
context "when no shipping method is present" do
before { expect(standing_order).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(standing_order).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(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:payment_method)) }
before { stub_validations(validator, validation_stubs.except(:payment_method_allowed?)) }
context "when no payment method is present" do
before { expect(standing_order).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(standing_order).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(:standing_order) { instance_double(StandingOrder, standing_order_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(standing_order).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) { StandingOrder::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(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:begins_at, :ends_at)) }
before { stub_validations(validator, validation_stubs.except(:ends_at_after_begins_at?)) }
before { expect(standing_order).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(standing_order).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(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:bill_address, :ship_address)) }
before { expect(standing_order).to receive(:bill_address).at_least(:once) { bill_address } }
before { expect(standing_order).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(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:customer)) }
before { stub_validations(validator, validation_stubs.except(:customer_allowed?)) }
before { expect(standing_order).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(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:schedule)) }
before { stub_validations(validator, validation_stubs.except(:schedule_allowed?)) }
before { expect(standing_order).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(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:payment_method)) }
before { stub_validations(validator, validation_stubs.except(:credit_card_ok?)) }
before { expect(standing_order).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[:standing_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(standing_order).to receive(:credit_card_id).at_least(:once) { credit_card_id } }
context "when a credit card is not present" do
let(:credit_card_id) { nil }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:credit_card]).to_not be_empty
end
end
context "when a credit card is present" do
let(:credit_card_id) { 12 }
before { expect(standing_order).to receive(:customer).at_least(:once) { customer } }
context "and the customer is not associated with a user" do
let(:customer) { instance_double(Customer, user: nil) }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:credit_card]).to_not be_empty
end
end
context "and the customer is associated with a user" do
let(:customer) { instance_double(Customer, user: user) }
context "and the user has no credit cards which match that specified" do
let(:user) { instance_double(Spree::User, credit_card_ids: [1, 2, 3, 4]) }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:credit_card]).to_not be_empty
end
end
context "and the user has a credit card which matches that specified" do
let(:user) { instance_double(Spree::User, credit_card_ids: [1, 2, 3, 12]) }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:credit_card]).to be_empty
end
end
end
end
end
end
describe "standing line items" do
let(:standing_order) { instance_double(StandingOrder, standing_order_stubs) }
before { stub_validations(validator, validation_stubs.except(:standing_line_items_present?)) }
before { expect(standing_order).to receive(:standing_line_items).at_least(:once) { standing_line_items } }
context "when no standing line items are present" do
let(:standing_line_items) { [] }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:standing_line_items]).to_not be_empty
end
end
context "when standing line items are present but they are all marked for destruction" do
let(:standing_line_item1) { instance_double(StandingLineItem, marked_for_destruction?: true) }
let(:standing_line_items) { [standing_line_item1] }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:standing_line_items]).to_not be_empty
end
end
context "when standing line items are present and some and not marked for destruction" do
let(:standing_line_item1) { instance_double(StandingLineItem, marked_for_destruction?: true) }
let(:standing_line_item2) { instance_double(StandingLineItem, marked_for_destruction?: false) }
let(:standing_line_items) { [standing_line_item1, standing_line_item2] }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:standing_line_items]).to be_empty
end
end
end
describe "variant availability" do
let(:standing_order) { instance_double(StandingOrder, standing_order_stubs) }
before { stub_validations(validator, validation_stubs.except(:requested_variants_available?)) }
before { expect(standing_order).to receive(:standing_line_items).at_least(:once) { standing_line_items } }
context "when no standing line items are present" do
let(:standing_line_items) { [] }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:standing_line_items]).to be_empty
end
end
context "when standing line items are present" do
let(:variant1) { instance_double(Spree::Variant, id: 1) }
let(:variant2) { instance_double(Spree::Variant, id: 2) }
let(:standing_line_item1) { instance_double(StandingLineItem, variant: variant1) }
let(:standing_line_item2) { instance_double(StandingLineItem, variant: variant2) }
let(:standing_line_items) { [standing_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[:standing_line_items]).to_not be_empty
end
end
context "and all requested variants are available" do
before { allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:standing_line_items]).to be_empty
end
end
end
end
end
end