Merge pull request #2733 from kristinalim/fix-subscription_should_not_require_future_oc

Reduce restrictions for adding a variant to a subscription
This commit is contained in:
Pau Pérez Fabregat
2019-02-06 21:24:57 +01:00
committed by GitHub
20 changed files with 528 additions and 159 deletions

View File

@@ -6,7 +6,11 @@ angular.module("admin.subscriptions").controller "SubscriptionController", ($sco
$scope.schedules = Schedules.all
$scope.paymentMethods = PaymentMethods.all
$scope.shippingMethods = ShippingMethods.all
$scope.distributor_id = $scope.subscription.shop_id # variant selector requires distributor_id
# Variant selector requires these
$scope.distributor_id = $scope.subscription.shop_id
$scope.eligible_for_subscriptions = true
$scope.view = if $scope.subscription.id? then 'review' else 'details'
$scope.nextCallbacks = {}
$scope.backCallbacks = {}

View File

@@ -22,6 +22,7 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) ->
q: term
distributor_id: scope.distributor_id
order_cycle_id: scope.order_cycle_id
eligible_for_subscriptions: scope.eligible_for_subscriptions
results: (data, page) ->
results: data
formatResult: (variant) ->

View File

@@ -0,0 +1,7 @@
@import '../variables';
.admin-subscription-form-subscription-line-items {
.not-in-open-and-upcoming-order-cycles-warning {
color: $warning-red;
}
}

View File

@@ -0,0 +1,7 @@
@import '../variables';
.admin-subscription-review-subscription-line-items {
.not-in-open-and-upcoming-order-cycles-warning {
color: $warning-red;
}
}

View File

@@ -13,7 +13,8 @@ module Admin
def build
@subscription_line_item.assign_attributes(params[:subscription_line_item])
@subscription_line_item.price_estimate = price_estimate
render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer
render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer,
shop: @shop, schedule: @schedule
end
private
@@ -26,7 +27,7 @@ module Admin
@shop = Enterprise.managed_by(spree_current_user).find_by_id(params[:shop_id])
@schedule = permissions.editable_schedules.find_by_id(params[:schedule_id])
@order_cycle = @schedule.andand.current_or_next_order_cycle
@variant = Spree::Variant.stockable_by(@shop).find_by_id(params[:subscription_line_item][:variant_id])
@variant = variant_if_eligible(params[:subscription_line_item][:variant_id]) if @shop.present?
end
def new_actions
@@ -50,5 +51,9 @@ module Admin
OpenFoodNetwork::ScopeVariantToHub.new(@shop).scope(@variant)
@variant.price + fee_calculator.indexed_fees_for(@variant)
end
def variant_if_eligible(variant_id)
SubscriptionVariantsService.eligible_variants(@shop).find_by_id(variant_id)
end
end
end

View File

@@ -1,7 +1,8 @@
module Api
module Admin
class SubscriptionLineItemSerializer < ActiveModel::Serializer
attributes :id, :variant_id, :quantity, :description, :price_estimate
attributes :id, :variant_id, :quantity, :description, :price_estimate,
:in_open_and_upcoming_order_cycles
def description
"#{object.variant.product.name} - #{object.variant.full_name}"
@@ -10,6 +11,22 @@ module Api
def price_estimate
object.price_estimate.andand.to_f || "?"
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)
end
private
def option_or_assigned_shop
@options[:shop] || object.subscription.andand.shop
end
def option_or_assigned_schedule
@options[:schedule] || object.subscription.andand.schedule
end
end
end
end

View File

@@ -97,15 +97,12 @@ class SubscriptionValidator
errors.add(:subscription_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: subscription_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)
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)

View File

@@ -0,0 +1,39 @@
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).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

@@ -56,7 +56,7 @@
%input#edit-products{ type: "button", value: t(:edit), ng: { click: "setView('products')" } }
.row
.seven.columns.alpha.omega
%table#subscription-line-items
%table#subscription-line-items.admin-subscription-review-subscription-line-items
%colgroup
%col{:style => "width: 62%;"}/
%col{:style => "width: 14%;"}/
@@ -71,7 +71,10 @@
%span= t(:total)
%tbody
%tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } }
%td.description {{ item.description }}
%td
.description {{ item.description }}
.not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } }
= t(".no_open_or_upcoming_order_cycle")
%td.price.align-center {{ item.price_estimate | currency }}
%td.quantity {{ item.quantity }}
%td.total.align-center {{ (item.price_estimate * item.quantity) | currency }}

View File

@@ -1,4 +1,4 @@
%table#subscription-line-items
%table#subscription-line-items.admin-subscription-form-subscription-line-items
%colgroup
%col{:style => "width: 49%;"}/
%col{:style => "width: 14%;"}/
@@ -15,7 +15,10 @@
%th.orders-actions.actions
%tbody
%tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } }
%td.description {{ item.description }}
%td
.description {{ item.description }}
.not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } }
= t(".not_in_open_and_upcoming_order_cycles_warning")
%td.price.align-center {{ item.price_estimate | currency }}
%td.quantity
%input{ name: 'quantity', type: 'number', min: 0, ng: { model: 'item.quantity' } }

View File

@@ -1088,6 +1088,7 @@ en:
this_is_an_estimate: |
The displayed prices are only an estimate and calculated at the time the subscription is changed.
If you change prices or fees, orders will be updated, but the subscription will still display the old values.
not_in_open_and_upcoming_order_cycles_warning: "There are no open or upcoming order cycles for this product."
details:
details: Details
invalid_error: Oops! Please fill in all of the required fields...
@@ -1102,6 +1103,7 @@ en:
details: Details
address: Address
products: Products
no_open_or_upcoming_order_cycle: "No Upcoming Order Cycle"
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
orders:
number: Number

View File

@@ -33,6 +33,10 @@ module OpenFoodNetwork
Spree::Variant.where(is_master: false).ransack(search_params.merge(m: 'or')).result
end
def distributor
Enterprise.find params[:distributor_id]
end
def scope_to_schedule
@variants = @variants.in_schedule(params[:schedule_id])
end
@@ -42,12 +46,29 @@ module OpenFoodNetwork
end
def scope_to_distributor
distributor = Enterprise.find params[:distributor_id]
if params[:eligible_for_subscriptions]
scope_to_eligible_for_subscriptions_in_distributor
else
scope_to_available_for_orders_in_distributor
end
end
def scope_to_available_for_orders_in_distributor
@variants = @variants.in_distributor(distributor)
scope_variants_to_distributor(@variants, distributor)
end
def scope_to_eligible_for_subscriptions_in_distributor
eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor)
@variants = @variants.merge(eligible_variants_scope)
scope_variants_to_distributor(@variants, distributor)
end
def scope_variants_to_distributor(variants, distributor)
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)
# Perform scoping after all filtering is done.
# Filtering could be a problem on scoped variants.
@variants.each { |v| scoper.scope(v) }
variants.each { |v| scoper.scope(v) }
end
end
end

View File

@@ -10,9 +10,9 @@ describe Admin::SubscriptionLineItemsController, type: :controller do
let(:unmanaged_shop) { create(:enterprise) }
let!(:product) { create(:product) }
let!(:variant) { create(:variant, product: product, unit_value: '100', price: 15.00, option_values: []) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 3.50) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_shop)]) }
@@ -42,6 +42,8 @@ describe Admin::SubscriptionLineItemsController, type: :controller do
before { params.merge!(shop_id: shop.id) }
context "but the shop doesn't have permission to sell product in question" do
let!(:outgoing_exchange) { }
it "returns an error" do
spree_post :build, params
json_response = JSON.parse(response.body)

View File

@@ -341,7 +341,7 @@ describe Admin::SubscriptionsController, type: :controller do
end
context 'with subscription_line_items params' do
let!(:product2) { create(:product, supplier: shop) }
let!(:product2) { create(:product) }
let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) }
before do

View File

@@ -145,23 +145,25 @@ feature 'Subscriptions' do
let!(:customer_user) { create(:user) }
let!(:credit_card1) { create(:credit_card, user: customer_user, cc_type: 'visa', last_digits: 1111, month: 10, year: 2030) }
let!(:customer) { create(:customer, enterprise: shop, bill_address: address, user: customer_user, allow_charges: true) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { 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!(:test_product) { create(:product, supplier: shop, distributors: []) }
let!(:test_variant) { create(:variant, product: test_product, unit_value: "100", price: 12.00, option_values: []) }
let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) }
let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "1000", price: 6.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [test_variant, shop_variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
it "passes the smoke test" do
before do
visit admin_subscriptions_path
click_link 'New Subscription'
select2_select shop.name, from: 'new_subscription_shop_id'
click_button 'Continue'
click_link "New Subscription"
select2_select shop.name, from: "new_subscription_shop_id"
click_button "Continue"
end
it "passes the smoke test" do
select2_select customer.email, from: 'customer_id'
select2_select schedule.name, from: 'schedule_id'
select2_select payment_method.name, from: 'payment_method_id'
@@ -215,11 +217,9 @@ feature 'Subscriptions' do
expect(page).to have_content 'Please add at least one product'
# Adding a product and getting a price estimate
select2_search_async product1.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 2
click_link 'Add'
add_variant_to_subscription test_variant, 2
within 'table#subscription-line-items tr.item', match: :first do
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector '.description', text: "#{test_product.name} - #{test_variant.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
@@ -241,11 +241,9 @@ feature 'Subscriptions' do
click_button('edit-products')
# Adding a new product
select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 3
click_link 'Add'
add_variant_to_subscription shop_variant, 3
within 'table#subscription-line-items tr.item', match: :first do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "3"
expect(page).to have_selector 'td.total', text: "$23.25"
@@ -264,7 +262,7 @@ feature 'Subscriptions' do
# Prices are shown in the index
within 'table#subscription-line-items tr.item', match: :first do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "3"
expect(page).to have_selector 'td.total', text: "$23.25"
@@ -282,142 +280,249 @@ feature 'Subscriptions' do
# Standing Line Items are created
expect(subscription.subscription_line_items.count).to eq 1
subscription_line_item = subscription.subscription_line_items.first
expect(subscription_line_item.variant).to eq variant2
expect(subscription_line_item.variant).to eq shop_variant
expect(subscription_line_item.quantity).to eq 3
end
end
context 'editing an existing subscription' do
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: product3, unit_value: '10000', price: 22.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:subscription) {
create(:subscription,
shop: shop,
customer: customer,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method,
subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)],
with_proxy_orders: true)
}
context 'editing an existing subscription' do
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: product3, unit_value: '10000', price: 22.00, option_values: []) }
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:subscription) {
create(:subscription,
shop: shop,
customer: customer,
schedule: schedule,
payment_method: payment_method,
shipping_method: shipping_method,
subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)],
with_proxy_orders: true)
}
it "passes the smoke test" do
visit edit_admin_subscription_path(subscription)
it "passes the smoke test" do
visit edit_admin_subscription_path(subscription)
# Customer and Schedule cannot be edited
click_button 'edit-details'
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
# Customer and Schedule cannot be edited
click_button 'edit-details'
expect(page).to have_selector '#s2id_customer_id.select2-container-disabled'
expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled'
# Can't use a Stripe payment method because customer does not allow it
select2_select stripe_payment_method.name, from: 'payment_method_id'
expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed')
click_button 'Save Changes'
expect(page).to have_content 'Credit card charges are not allowed by this customer'
select2_select payment_method.name, from: 'payment_method_id'
click_button 'Review'
# Can't use a Stripe payment method because customer does not allow it
select2_select stripe_payment_method.name, from: 'payment_method_id'
expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed')
click_button 'Save Changes'
expect(page).to have_content 'Credit card charges are not allowed by this customer'
select2_select payment_method.name, from: 'payment_method_id'
click_button 'Review'
# Existing products should be visible
click_button 'edit-products'
within "#sli_0" do
expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
# Existing products should be visible
click_button 'edit-products'
within "#sli_0" do
expect(page).to have_selector '.description', text: "#{product1.name} - #{variant1.full_name}"
expect(page).to have_selector 'td.price', text: "$13.75"
expect(page).to have_input 'quantity', with: "2"
expect(page).to have_selector 'td.total', text: "$27.50"
# Remove variant1 from the subscription
find("a.delete-item").click
end
# Attempting to submit without a product
click_button 'Save Changes'
expect(page).to have_content 'Please add at least one product'
# Add variant2 to the subscription
select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 1
click_link 'Add'
within "#sli_0" do
expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$7.75"
end
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
# Add variant3 to the subscription (even though it is not available)
select2_search_async product3.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop'
fill_in 'add_quantity', with: 1
click_link 'Add'
within "#sli_1" do
expect(page).to have_selector 'td.description', text: "#{product3.name} - #{variant3.full_name}"
expect(page).to have_selector 'td.price', text: "$22.00"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$22.00"
end
# Total should be $29.75
expect(page).to have_selector '#order_form_total', text: "$29.75"
click_button 'Save Changes'
expect(page).to have_content "#{product3.name} - #{variant3.full_name} is not available from the selected schedule"
# Remove variant3 from the subscription
within '#sli_1' do
find("a.delete-item").click
end
click_button 'Save Changes'
expect(page).to have_current_path admin_subscriptions_path
select2_select shop.name, from: "shop_id"
expect(page).to have_selector "td.items.panel-toggle"
first("td.items.panel-toggle").click
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
expect(page).to have_selector 'tr.item', count: 1
expect(subscription.reload.subscription_line_items.length).to eq 1
expect(subscription.subscription_line_items.first.variant).to eq variant2
# Remove variant1 from the subscription
find("a.delete-item").click
end
context "with initialised order that has been changed" do
let(:proxy_order) { subscription.proxy_orders.first }
let(:order) { proxy_order.initialise_order! }
let(:line_item) { order.line_items.first }
# Attempting to submit without a product
click_button 'Save Changes'
expect(page).to have_content 'Please add at least one product'
before { line_item.update_attributes(quantity: 3) }
# Add variant2 to the subscription
add_variant_to_subscription(variant2, 1)
within "#sli_0" do
expect(page).to have_selector '.description', text: "#{product2.name} - #{variant2.full_name}"
expect(page).to have_selector 'td.price', text: "$7.75"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$7.75"
end
it "reports issues encountered during the update" do
visit edit_admin_subscription_path(subscription)
click_button 'edit-products'
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
within "#sli_0" do
fill_in 'quantity', with: "1"
end
# Add variant3 to the subscription (even though it is not available)
add_variant_to_subscription(variant3, 1)
within "#sli_1" do
expect(page).to have_selector '.description', text: "#{product3.name} - #{variant3.full_name}"
expect(page).to have_selector 'td.price', text: "$22.00"
expect(page).to have_input 'quantity', with: "1"
expect(page).to have_selector 'td.total', text: "$22.00"
end
click_button 'Save Changes'
expect(page).to have_content 'Saved'
# Total should be $29.75
expect(page).to have_selector '#order_form_total', text: "$29.75"
expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg")
# Remove variant3 from the subscription
within '#sli_1' do
find("a.delete-item").click
end
click_button 'Save Changes'
expect(page).to have_current_path admin_subscriptions_path
select2_select shop.name, from: "shop_id"
expect(page).to have_selector "td.items.panel-toggle"
first("td.items.panel-toggle").click
# Total should be $7.75
expect(page).to have_selector '#order_form_total', text: "$7.75"
expect(page).to have_selector 'tr.item', count: 1
expect(subscription.reload.subscription_line_items.length).to eq 1
expect(subscription.subscription_line_items.first.variant).to eq variant2
end
context "with initialised order that has been changed" do
let(:proxy_order) { subscription.proxy_orders.first }
let(:order) { proxy_order.initialise_order! }
let(:line_item) { order.line_items.first }
before { line_item.update_attributes(quantity: 3) }
it "reports issues encountered during the update" do
visit edit_admin_subscription_path(subscription)
click_button 'edit-products'
within "#sli_0" do
fill_in 'quantity', with: "1"
end
click_button 'Save Changes'
expect(page).to have_content 'Saved'
expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg")
end
end
end
describe "allowed variants" do
let!(:customer) { create(:customer, enterprise: shop, allow_charges: true) }
let!(:credit_card) { create(:credit_card, user: customer.user) }
let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) }
let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "2000") }
let!(:permitted_supplier) do
create(:supplier_enterprise).tap do |supplier|
create(:enterprise_relationship, child: shop, parent: supplier, permissions_list: [:add_to_order_cycle])
end
end
let!(:permitted_supplier_product) { create(:product, supplier: permitted_supplier, distributors: [shop]) }
let!(:permitted_supplier_variant) { create(:variant, product: permitted_supplier_product, unit_value: "2000") }
let!(:incoming_exchange_product) { create(:product, distributors: [shop]) }
let!(:incoming_exchange_variant) do
create(:variant, product: incoming_exchange_product, unit_value: "2000").tap do |variant|
create(:exchange, order_cycle: order_cycle, incoming: true, receiver: shop, variants: [variant])
end
end
let!(:outgoing_exchange_product) { create(:product, distributors: [shop]) }
let!(:outgoing_exchange_variant) do
create(:variant, product: outgoing_exchange_product, unit_value: "2000").tap do |variant|
create(:exchange, order_cycle: order_cycle, incoming: false, receiver: shop, variants: [variant])
end
end
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop], preferred_enterprise_id: shop.id) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
before do
visit admin_subscriptions_path
click_link "New Subscription"
select2_select shop.name, from: "new_subscription_shop_id"
click_button "Continue"
end
it "permit creating and editing of the subscription" do
# Fill in other details
fill_in_subscription_basic_details
click_button "Next"
expect(page).to have_content "BILLING ADDRESS"
click_button "Next"
# Add products
expect(page).to have_content "NAME OR SKU"
add_variant_to_subscription shop_variant, 3
expect_not_in_open_or_upcoming_order_cycle_warning 1
add_variant_to_subscription permitted_supplier_variant, 4
expect_not_in_open_or_upcoming_order_cycle_warning 2
add_variant_to_subscription incoming_exchange_variant, 5
expect_not_in_open_or_upcoming_order_cycle_warning 3
add_variant_to_subscription outgoing_exchange_variant, 6
expect_not_in_open_or_upcoming_order_cycle_warning 3
click_button "Next"
# Submit form
expect {
click_button "Create Subscription"
expect(page).to have_current_path admin_subscriptions_path
}.to change(Subscription, :count).by(1)
# Subscription line items are created
subscription = Subscription.last
expect(subscription.subscription_line_items.count).to eq 4
# Edit the subscription
visit edit_admin_subscription_path(subscription)
# Remove shop_variant from the subscription
click_button "edit-products"
within "#sli_0" do
expect(page).to have_selector ".description", text: shop_variant.name
find("a.delete-item").click
end
# Submit form
click_button "Save Changes"
expect(page).to have_current_path admin_subscriptions_path
# Subscription is saved
visit edit_admin_subscription_path(subscription)
expect(page).to have_selector "#subscription-line-items .item", count: 3
end
end
end
def fill_in_subscription_basic_details
select2_select customer.email, from: "customer_id"
select2_select schedule.name, from: "schedule_id"
select2_select payment_method.name, from: "payment_method_id"
select2_select shipping_method.name, from: "shipping_method_id"
find_field("begins_at").click
choose_today_from_datepicker
end
def expect_not_in_open_or_upcoming_order_cycle_warning(count)
expect(page).to have_content variant_not_in_open_or_upcoming_order_cycle_warning, count: count
end
def add_variant_to_subscription(variant, quantity)
row_count = all("#subscription-line-items .item").length
variant_name = variant.full_name.present? ? "#{variant.name} - #{variant.full_name}" : variant.name
select2_search variant.name, from: I18n.t(:name_or_sku), dropdown_css: ".select2-drop", select_text: variant_name
fill_in "add_quantity", with: quantity
click_link "Add"
expect(page).to have_selector("#subscription-line-items .item", count: row_count + 1)
end
def variant_not_in_open_or_upcoming_order_cycle_warning
I18n.t("not_in_open_and_upcoming_order_cycles_warning",
scope: "admin.subscriptions.subscription_line_items")
end
end

View File

@@ -1,8 +1,11 @@
require "spec_helper"
describe SubscriptionValidator do
let(:shop) { instance_double(Enterprise, name: "Shop") }
let(:owner) { create(:user) }
let(:shop) { create(:enterprise, name: "Shop", owner: owner) }
describe "delegation" do
let(:subscription) { create(:subscription) }
let(:subscription) { create(:subscription, shop: shop) }
let(:validator) { SubscriptionValidator.new(subscription) }
it "delegates to subscription" do
@@ -438,6 +441,7 @@ describe SubscriptionValidator do
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 }
@@ -451,7 +455,9 @@ describe SubscriptionValidator do
end
context "and all requested variants are available" do
before { allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } }
before do
allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] }
end
it "returns true" do
expect(validator.valid?).to be true

View File

@@ -0,0 +1,130 @@
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

@@ -136,6 +136,7 @@ RSpec.configure do |config|
config.extend Spree::Api::TestingSupport::Setup, :type => :controller
config.include Spree::Api::TestingSupport::Helpers, :type => :controller
config.include OpenFoodNetwork::ControllerHelper, :type => :controller
config.include Features::DatepickerHelper, type: :feature
config.include OpenFoodNetwork::FeatureToggleHelper
config.include OpenFoodNetwork::FiltersHelper
config.include OpenFoodNetwork::EnterpriseGroupsHelper

View File

@@ -0,0 +1,9 @@
module Features
module DatepickerHelper
def choose_today_from_datepicker
within(".ui-datepicker-calendar") do
find(".ui-datepicker-today").click
end
end
end
end

View File

@@ -105,6 +105,16 @@ module WebHelper
targetted_select2(value, options)
end
# Support having different texts to search for and to click in the select2
# field.
#
# This overrides the method in Spree.
def targetted_select2_search(value, options)
page.execute_script %Q{$('#{options[:from]}').select2('open')}
page.execute_script "$('#{options[:dropdown_css]} input.select2-input').val('#{value}').trigger('keyup-change');"
select_select2_result(options[:select_text] || value)
end
def multi_select2_select(value, options)
find("#s2id_#{options[:from]}").find('ul li.select2-search-field').click
select_select2_result(value)