openfoodfoundation/openfoodnetwork

View on GitHub
engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb

Summary

Maintainability
A
0 mins
Test Coverage
# 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:) }

      describe "delegation" do
        let(:subscription) { create(:subscription, 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:,
            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]).not_to 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]).not_to 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]).not_to 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]).not_to 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]).not_to 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]).not_to 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]).not_to 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]).not_to 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]).not_to be_empty
              expect(validator.errors[:ship_address]).not_to 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]).not_to 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]).not_to 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]).not_to 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]).not_to 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 StripeSCA payment gateway" do
            let(:payment_method) {
              instance_double(Spree::PaymentMethod, type: "Spree::Gateway::StripeSCA")
            }
            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]).not_to 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]).not_to 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]).not_to 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]).not_to be_empty
            end
          end

          context "when some subscription line items exist" do
            context "and they have some quantity but all are marked for destruction" do
              let(:subscription_line_item1) {
                instance_double(SubscriptionLineItem, marked_for_destruction?: true, quantity: 1)
              }
              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]).not_to be_empty
              end
            end

            context "and at least one has some quantity and is not marked for destruction" do
              let(:subscription_line_item1) {
                instance_double(SubscriptionLineItem, marked_for_destruction?: true, quantity: 1)
              }
              let(:subscription_line_item2) {
                instance_double(SubscriptionLineItem, marked_for_destruction?: false, quantity: 1)
              }
              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

            context "and they are not marked for destruction but all have no quantity" do
              let(:subscription_line_item1) {
                instance_double(SubscriptionLineItem, marked_for_destruction?: false, quantity: 0)
              }
              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]).not_to be_empty
              end
            end

            context "but all have no quantity and are marked for destruction" do
              let(:subscription_line_item1) {
                instance_double(SubscriptionLineItem, marked_for_destruction?: true, quantity: 0)
              }
              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]).not_to be_empty
              end
            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, deleted_at: nil) }
            let(:variant2) { instance_double(Spree::Variant, id: 2) }
            let(:variant3) { instance_double(Spree::Variant, id: 1, deleted_at: Time.zone.now ) }
            let(:subscription_line_item1) {
              instance_double(SubscriptionLineItem,
                              variant: variant1,
                              marked_for_destruction?: false)
            }
            let(:subscription_line_item2) {
              instance_double(SubscriptionLineItem,
                              variant: variant2,
                              marked_for_destruction?: false)
            }
            let(:subscription_line_item3) {
              instance_double(SubscriptionLineItem,
                              variant: variant3,
                              marked_for_destruction?: true)
            }
            let(:subscription_line_items) { [subscription_line_item1, subscription_line_item3] }

            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]).not_to 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