Skip to content

Commit

Permalink
[SD-1447] Checkout unshippable items endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
szymoniwacz committed Sep 22, 2021
1 parent 14fff7f commit 90b61e7
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 2 deletions.
25 changes: 25 additions & 0 deletions api/app/controllers/spree/api/v2/storefront/checkout_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ def shipping_rates
end
end

def unshippable_items
result = unshippable_items_service.call(order: spree_current_order)

if result.success?
render_serialized_payload { serialize_unshippable_items(result.value) }
else
render_error_payload(result.error)
end
end

def payment_methods
render_serialized_payload { serialize_payment_methods(spree_current_order.available_payment_methods) }
end
Expand Down Expand Up @@ -129,6 +139,21 @@ def serialize_shipping_rates(shipments)
include: [:shipping_rates, :stock_location]
).serializable_hash
end

def unshippable_items_service
Spree::Api::Dependencies.storefront_checkout_get_unshippable_items_service.constantize
end

def line_items_serializer
Spree::V2::Storefront::LineItemSerializer
end

def serialize_unshippable_items(line_items)
line_items_serializer.new(
line_items,
params: serializer_params
).serializable_hash
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion api/app/models/spree/api_dependencies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class ApiDependencies
:storefront_store_serializer, :storefront_address_serializer, :storefront_order_serializer,
:storefront_account_create_address_service, :storefront_account_update_address_service, :storefront_address_finder,
:storefront_account_create_service, :storefront_account_update_service, :storefront_collection_sorter, :error_handler,
:storefront_cart_empty_service, :storefront_cart_destroy_service, :storefront_credit_cards_destroy_service, :platform_products_sorter
:storefront_cart_empty_service, :storefront_cart_destroy_service, :storefront_credit_cards_destroy_service, :platform_products_sorter,
:storefront_checkout_get_unshippable_items_service
].freeze

attr_accessor *INJECTION_POINTS
Expand Down Expand Up @@ -56,6 +57,7 @@ def set_storefront_defaults
@storefront_checkout_add_store_credit_service = Spree::Dependencies.checkout_add_store_credit_service
@storefront_checkout_remove_store_credit_service = Spree::Dependencies.checkout_remove_store_credit_service
@storefront_checkout_get_shipping_rates_service = Spree::Dependencies.checkout_get_shipping_rates_service
@storefront_checkout_get_unshippable_items_service = Spree::Dependencies.checkout_get_unshippable_items_service

# account services
@storefront_account_create_service = Spree::Dependencies.account_create_service
Expand Down
1 change: 1 addition & 0 deletions api/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
post :remove_store_credit
get :payment_methods
get :shipping_rates
get :unshippable_items
end

resource :account, controller: :account, only: %i[show create update]
Expand Down
142 changes: 142 additions & 0 deletions api/spec/requests/spree/api/v2/storefront/checkout_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,148 @@
end
end

describe 'checkout#unshippable_items' do
let(:execute) { get '/api/v2/storefront/checkout/unshippable_items', headers: headers }

let(:country) { store.default_country }
let(:zone) { create(:zone, name: 'US') }
let(:shipping_method) { create(:shipping_method) }
let(:shipment) { order.shipments.first }

shared_examples 'showing only not shippable line items' do
shared_examples 'returns valid line item JSON' do
it 'returns valid line item JSON' do
expect(json_response['data'][0]['id']).to eq(check_item.id.to_s)
expect(json_response['data'][0]['type']).to eq('line_item')
expect(json_response['data'][0]).to have_attribute(:name).with_value(check_item.name)
expect(json_response['data'][0]).to have_attribute(:quantity).with_value(check_item.quantity)
expect(json_response['data'][0]).to have_attribute(:slug).with_value(check_item.slug)
expect(json_response['data'][0]).to have_attribute(:options_text).with_value(check_item.options_text)
expect(json_response['data'][0]).to have_attribute(:currency).with_value(check_item.currency)
expect(json_response['data'][0]).to have_attribute(:price).with_value(check_item.price.to_s)
expect(json_response['data'][0]).to have_attribute(:display_price).with_value(check_item.display_price.to_s)
expect(json_response['data'][0]).to have_attribute(:total).with_value(check_item.total.to_s)
expect(json_response['data'][0]).to have_attribute(:display_total).with_value(check_item.display_total.to_s)
expect(json_response['data'][0]).to have_attribute(:adjustment_total).with_value(check_item.adjustment_total.to_s)
expect(json_response['data'][0]).to have_attribute(:display_adjustment_total).with_value(check_item.display_adjustment_total.to_s)
expect(json_response['data'][0]).to have_attribute(:additional_tax_total).with_value(check_item.additional_tax_total.to_s)
expect(json_response['data'][0]).to have_attribute(:discounted_amount).with_value(check_item.discounted_amount.to_s)
expect(json_response['data'][0]).to have_attribute(:display_discounted_amount).with_value(check_item.display_discounted_amount.to_s)
expect(json_response['data'][0]).to have_attribute(:display_additional_tax_total).with_value(check_item.display_additional_tax_total.to_s)
expect(json_response['data'][0]).to have_attribute(:promo_total).with_value(check_item.promo_total.to_s)
expect(json_response['data'][0]).to have_attribute(:display_promo_total).with_value(check_item.display_promo_total.to_s)
expect(json_response['data'][0]).to have_attribute(:included_tax_total).with_value(check_item.included_tax_total.to_s)
expect(json_response['data'][0]).to have_attribute(:display_included_tax_total).with_value(check_item.display_included_tax_total.to_s)
expect(json_response['data'][0]).to have_attribute(:pre_tax_amount).with_value(check_item.pre_tax_amount.to_s)
expect(json_response['data'][0]).to have_attribute(:display_pre_tax_amount).with_value(check_item.display_pre_tax_amount.to_s)
expect(json_response['data'][0]).to have_relationship(:variant).with_data({ 'id' => check_item.variant.id.to_s, 'type' => 'variant' })
end
end

shared_examples 'returns empty collection' do
it_behaves_like 'returns 200 HTTP status'

it 'should not return any line items' do
expect(json_response['data']).to be_empty
end
end

before do
order.shipping_address = address
order.save!
zone.countries << country
shipping_method.zones = [zone]
end

context 'when all order items are shippable' do
let(:address) { create(:address, country: country) }

before { execute }

it_behaves_like 'returns empty collection'
end

context 'when all order items are not shippable' do
let(:address) { create(:address, country: create(:country, iso: 'AD'), zipcode: 'AD500') }
let(:check_item) { line_item }

before { execute }

it_behaves_like 'returns 200 HTTP status'

it 'should return one line item', aggregate_failures: true do
expect(order.line_items.count).to eq(1)
end

it_behaves_like 'returns valid line item JSON'
end

context 'when one order item is not shippable' do
let(:zw_shipping_category) { create(:shipping_category) }
let(:zw_shipping_method) { create(:shipping_method, shipping_categories: [zw_shipping_category]) }
let(:zw_zone) { create(:zone, name: 'ZW') }
let(:zw_country) { create(:country, iso: 'ZW') }
let(:zw_product) { create(:product, shipping_category: zw_shipping_category) }
let!(:zw_line_item) { create(:line_item, order: order, product: zw_product, currency: currency) }
let(:address) { create(:address, country: country) }
let(:check_item) { zw_line_item }

before do
zw_zone.countries << zw_country
zw_shipping_method.zones = [zw_zone]
ensure_order_totals
execute
end

it_behaves_like 'returns 200 HTTP status'

it 'should return all line items', aggregate_failures: true do
expect(order.line_items.count).to eq(2)
expect(json_response['data'].count).to eq(1)
end

it_behaves_like 'returns valid line item JSON'
end

context 'when shipping address zone not found' do
let(:address) { create(:address, country: country) }

before do
allow(Spree::Zone).to receive(:match).with(address).and_return(nil)
execute
end

it_behaves_like 'returns empty collection'
end
end

shared_examples 'showing 404' do
before do
get '/api/v2/storefront/checkout/unshippable_items', headers: headers
end

it_behaves_like 'returns 404 HTTP status'
end

context 'without existing order' do
let!(:headers) { headers_bearer }

it_behaves_like 'showing 404'
end

context 'with existing user order with line item' do
include_context 'creates order with line item'

it_behaves_like 'showing only not shippable line items'
end

context 'with existing guest order' do
include_context 'creates guest order with guest token'

it_behaves_like 'showing only not shippable line items'
end
end

describe 'full checkout flow' do
let!(:country) { create(:country) }
let(:state) { create(:state, country: country) }
Expand Down
3 changes: 2 additions & 1 deletion core/app/models/spree/app_dependencies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class AppDependencies
:products_finder, :taxon_finder, :line_item_by_variant_finder, :cart_estimate_shipping_rates_service,
:account_create_address_service, :account_update_address_service, :account_create_service, :account_update_service,
:address_finder, :collection_sorter, :error_handler, :current_store_finder, :cart_empty_service, :cart_destroy_service,
:classification_reposition_service, :credit_cards_destroy_service, :cart_associate_service
:classification_reposition_service, :credit_cards_destroy_service, :cart_associate_service, :checkout_get_unshippable_items_service
].freeze

attr_accessor *INJECTION_POINTS
Expand Down Expand Up @@ -53,6 +53,7 @@ def set_default_services
@checkout_add_store_credit_service = 'Spree::Checkout::AddStoreCredit'
@checkout_remove_store_credit_service = 'Spree::Checkout::RemoveStoreCredit'
@checkout_get_shipping_rates_service = 'Spree::Checkout::GetShippingRates'
@checkout_get_unshippable_items_service = 'Spree::Checkout::GetUnshippableItems'

# sorter
@collection_sorter = 'Spree::BaseSorter'
Expand Down
50 changes: 50 additions & 0 deletions core/app/services/spree/checkout/get_unshippable_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Spree
module Checkout
class GetUnshippableItems
prepend Spree::ServiceModule::Base

def call(order:)
run :reload_order
run :ensure_shipping_address
run :ensure_line_items_present
run :return_unshippable_items
end

private

def reload_order(order:)
success(order: order.reload)
end

def ensure_shipping_address(order:)
return failure([], Spree.t('errors.services.get_unshippable_items.no_shipping_address')) if order.ship_address.blank?

success(order: order)
end

def ensure_line_items_present(order:)
return failure([], Spree.t('errors.services.get_shipping_rates.no_line_items')) if order.line_items.empty?

success(order: order)
end

def return_unshippable_items(order:)
success(unshippable_items(order: order))
end

protected

def unshippable_items(order:)
# TODO: Use `order.store.checkout_zone`
shipping_address_zone_id = Spree::Zone.match(order.shipping_address)&.id
return [] if shipping_address_zone_id.nil?

order.line_items.select do |item|
item.variant.shipping_category.shipping_methods.any? do |sm|
sm.zone_ids.exclude?(shipping_address_zone_id)
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,9 @@ en:
get_shipping_rates:
no_shipping_address: To generate Shipping Rates Order needs to have a Shipping Address
no_line_items: To generate Shipping Rates you need to add some Line Items to Order
get_unshippable_items:
no_shipping_address: To look for Unshippable Items Order needs to have a Shipping Address
no_line_items: To look for Unshippable Items you need to add some Line Items to Order
errors_prohibited_this_record_from_being_saved:
one: 1 error prohibited this record from being saved
other: ! '%{count} errors prohibited this record from being saved'
Expand Down

0 comments on commit 90b61e7

Please sign in to comment.