- <%= f.field_container :name do %>
- <%= f.label :name %>
- <%= f.text_field :name, class: 'form-control' %>
- <% end %>
-
-
- <%= f.field_container :image do %>
- <%= f.label :image %>
- <%= f.file_field :image %>
- <%= image_tag main_app.url_for(@vendor.image.url(:small)) if @vendor.image %>
- <% end %>
-
-
- <%= f.field_container :about_us do %>
- <%= f.label :about_us %>
- <%= f.text_area :about_us, class: 'form-control' %>
- <% end %>
-
-
- <%= f.field_container :contact_us do %>
- <%= f.label :contact_us %>
- <%= f.text_area :contact_us, class: 'form-control' %>
- <% end %>
-
-
- <%= f.field_container :notification_email do %>
- <%= f.label :notification_email %>
- <%= f.email_field :notification_email, class: 'form-control' %>
- <% end %>
-
-
- <%= button Spree.t('actions.update'), 'save' %>
-
<%= Spree.t(:or) %>
- <%= button_link_to Spree.t('actions.cancel'), admin_vendor_settings_url, icon: 'delete' %>
+<%= content_for :page_actions do %>
+ <%= turbo_save_button_tag Spree.t('actions.update'), form: 'edit_vendor_settings' %>
+<% end %>
+
+<%= form_with model: current_vendor, url: spree.admin_vendor_settings_path, id: 'edit_vendor_settings' do |f| %>
+
+
+
+
+
+
+ <%= f.label :name %>
+ <%= f.text_field :name, class: 'form-control' %>
+
+
+ <%= f.label :billing_email %>
+ <%= f.email_field :billing_email, class: 'form-control' %>
+
+
+
+
+
+
+
+ <% f.object.build_billing_address(country: current_store.default_country) unless f.object.billing_address %>
+ <%= f.fields_for :billing_address do |billing_address_form| %>
+ <%= render 'spree/addresses/form',
+ address_name: 'billing_address',
+ address_form: billing_address_form,
+ address_type: 'billing',
+ address: f.object.billing_address,
+ available_countries: Spree::Country.all %>
+ <% end %>
+
+
+
+
+
+
+ <% f.object.build_returns_address(country: current_store.default_country) unless f.object.returns_address %>
+ <%= f.fields_for :returns_address do |returns_address_form| %>
+ <%= render 'spree/addresses/form',
+ address_name: 'returns_address',
+ address_form: returns_address_form,
+ address_type: 'shipping',
+ address: f.object.returns_address,
+ available_countries: Spree::Country.all %>
+ <% end %>
+
+
-
+
<% end %>
diff --git a/app/views/spree/admin/vendors/_billing.html.erb b/app/views/spree/admin/vendors/_billing.html.erb
new file mode 100644
index 00000000..cc395ad2
--- /dev/null
+++ b/app/views/spree/admin/vendors/_billing.html.erb
@@ -0,0 +1,36 @@
+
+
+
+
+ <% if @vendor.billing_email.present? %>
+
<%= mail_to @vendor.billing_email %>
+ <% end %>
+
+ <% if @vendor.billing_address.present? %>
+ <%= render 'spree/admin/shared/address', address: @vendor.billing_address %>
+ <% else %>
+
+ <%= Spree.t(:not_provided) %>
+
+ <% end %>
+
+
+
diff --git a/app/views/spree/admin/vendors/_card.html.erb b/app/views/spree/admin/vendors/_card.html.erb
new file mode 100644
index 00000000..4210dc6b
--- /dev/null
+++ b/app/views/spree/admin/vendors/_card.html.erb
@@ -0,0 +1,45 @@
+<% cache spree_base_cache_scope.call(@vendor) do %>
+ <%= turbo_frame_tag "vendor_card_#{@vendor.id}" do %>
+
+
+
+ <%= link_to_with_icon 'edit', Spree.t(:edit), spree.edit_brand_admin_vendor_path(@vendor), class: 'btn btn-light', data: { turbo_frame: :dialog_modal_lg } %>
+
+
+ <% if @vendor.cover_photo.attached? && @vendor.cover_photo.variable? %>
+ <%= image_tag(
+ main_app.cdn_image_url(
+ @vendor.cover_photo.variant(spree_image_variant_options(resize_to_fill: [800, 300]))
+ ),
+ class: 'card-img-top bg-light',
+ alt: @vendor.name,
+ style: 'height: 260px'
+ ) %>
+ <% else %>
+
+ <% end %>
+
+
+
+
+ <% if @vendor.logo.attached? && @vendor.logo.variable? %>
+ <%= image_tag(
+ main_app.cdn_image_url(
+ @vendor.logo.variant(spree_image_variant_options(resize_to_limit: [150, 150]))
+ ),
+ alt: @vendor.name,
+ class: 'rounded-lg'
+ ) %>
+ <% end %>
+
+
+
+ <% if @vendor.about.present? %>
+ <%= @vendor.about %>
+ <% end %>
+
+
+
+
+ <% end %>
+<% end %>
diff --git a/app/views/spree/admin/vendors/_contact.html.erb b/app/views/spree/admin/vendors/_contact.html.erb
new file mode 100644
index 00000000..3e45e294
--- /dev/null
+++ b/app/views/spree/admin/vendors/_contact.html.erb
@@ -0,0 +1,7 @@
+
+ <%= render_avatar(contact, width: 48, height: 48, class: 'avatar mb-0 m-1 rounded-lg float-left mr-3') %>
+
+ <%= contact.name&.truncate(40) %>
+ <%= contact.email.truncate(40) %>
+
+
\ No newline at end of file
diff --git a/app/views/spree/admin/vendors/_display_commission_rate.html.erb b/app/views/spree/admin/vendors/_display_commission_rate.html.erb
new file mode 100644
index 00000000..3f0c9550
--- /dev/null
+++ b/app/views/spree/admin/vendors/_display_commission_rate.html.erb
@@ -0,0 +1,12 @@
+<%= turbo_frame_tag "vendor_commision_rate_#{vendor.id}" do %>
+
+ Commission rate
+
+ <%= vendor.platform_fee %>%
+
+ <%= link_to_with_icon 'edit', Spree.t(:edit), spree.edit_commission_rate_admin_vendor_path(@vendor), \
+ class: 'btn btn-light btn-sm', no_text: true if can?(:admin, Spree::Store)
+ %>
+
+ <% end %>
+
diff --git a/app/views/spree/admin/vendors/_display_payouts_schedule.html.erb b/app/views/spree/admin/vendors/_display_payouts_schedule.html.erb
new file mode 100644
index 00000000..f24e50ca
--- /dev/null
+++ b/app/views/spree/admin/vendors/_display_payouts_schedule.html.erb
@@ -0,0 +1,7 @@
+<%= turbo_frame_tag "vendor_payouts_schedule_#{@vendor.id}" do %>
+
+ <%= @stripe_account.payouts_schedule&.capitalize %>
+
+ <%= link_to_with_icon 'edit', Spree.t(:edit), spree.edit_payouts_schedule_admin_vendor_path(@vendor), class: 'btn btn-light btn-sm' %>
+
+<% end if @stripe_account %>
diff --git a/app/views/spree/admin/vendors/_filters.html.erb b/app/views/spree/admin/vendors/_filters.html.erb
new file mode 100644
index 00000000..2fd8ab3c
--- /dev/null
+++ b/app/views/spree/admin/vendors/_filters.html.erb
@@ -0,0 +1,10 @@
+<%= search_form_for [:admin, @search], class: "filter-wrap", data: {controller: "filters"} do |f| %>
+
+ <%= render 'spree/admin/shared/filters_search_bar', param: :name_cont, label: Spree.t(:name) %>
+
+ <%= render "spree/admin/vendors/table_filter_dropdown" %>
+
+ <%= render "spree/admin/shared/filter_badge_template" %>
+
+
+<% end %>
diff --git a/app/views/spree/admin/vendors/_form.html.erb b/app/views/spree/admin/vendors/_form.html.erb
deleted file mode 100644
index 11ea2414..00000000
--- a/app/views/spree/admin/vendors/_form.html.erb
+++ /dev/null
@@ -1,34 +0,0 @@
-<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @vendor } %>
-
- <%= f.field_container :name do %>
- <%= f.label :name, raw(Spree.t(:name) + required_span_tag) %>
- <%= f.text_field :name, class: 'form-control' %>
- <% end %>
- <%= f.field_container :about_us do %>
- <%= f.label :about_us %>
- <%= f.text_area :about_us, class: 'form-control' %>
- <% end %>
- <%= f.field_container :contact_us do %>
- <%= f.label :contact_us %>
- <%= f.text_area :contact_us, class: 'form-control' %>
- <% end %>
- <%= f.field_container :state do %>
- <%= f.label :state, Spree.t(:state) %>
- <%= f.select :state,
- options_for_select(vendor_state_options, selected: @vendor.state),
- {}, class: 'select2', required: :required %>
- <% end %>
- <%= f.field_container :image do %>
- <%= f.label :image %>
- <%= f.file_field :image %>
- <%= image_tag main_app.url_for(@vendor.image.url(:small)) if @vendor.image %>
- <% end %>
- <%= f.field_container :commission_rate do %>
- <%= f.label :commission_rate %>
- <%= f.number_field :commission_rate, step: '0.01', class: 'form-control' %>
- <% end %>
- <%= f.field_container :notification_email do%>
- <%= f.label :notification_email %>
- <%= f.email_field :notification_email, class: 'form-control' %>
- <% end %>
-
diff --git a/app/views/spree/admin/vendors/_header.html.erb b/app/views/spree/admin/vendors/_header.html.erb
new file mode 100644
index 00000000..03e32c43
--- /dev/null
+++ b/app/views/spree/admin/vendors/_header.html.erb
@@ -0,0 +1,24 @@
+<% content_for :page_title do %>
+ <%= page_header_back_button spree.admin_vendors_url, @vendor unless current_vendor.present? %>
+ <%= @vendor.display_name %>
+ <%= vendor_status_badge(@vendor, class: 'small d-none d-lg-inline') %>
+<% end %>
+
+<% content_for :page_actions do %>
+ <% if @vendor.can_approve? %>
+ <%= link_to_with_icon 'check', Spree.t('actions.approve'), spree.approve_admin_vendor_path(@vendor), { class: "btn btn-primary mr-2", data: { turbo_method: :patch, turbo_confirm: Spree.t(:are_you_sure) } } if can?(:approve, @vendor) %>
+ <% end %>
+<% end if can?(:edit, @vendor) && controller_name == 'vendors' %>
+
+<% content_for :page_actions_dropdown do %>
+ <% if @vendor.can_reject? %>
+ <%= link_to_with_icon 'ban', Spree.t('actions.reject'), spree.reject_admin_vendor_path(@vendor), { class: "dropdown-item", data: { turbo_method: :patch, turbo_confirm: Spree.t(:are_you_sure) } } if can?(:reject, @vendor) %>
+ <% end %>
+ <% if @vendor.can_suspend? %>
+ <%= link_to_with_icon 'clock-pause', Spree.t('actions.suspend'), spree.suspend_admin_vendor_path(@vendor), { class: "dropdown-item", data: { turbo_method: :patch, turbo_confirm: Spree.t(:are_you_sure) } } if can?(:suspend, @vendor) %>
+ <% end %>
+ <% if can?(:delete, @vendor) && @vendor.can_be_deleted? %>
+ <%= link_to_with_icon 'archive', Spree.t('actions.archive'), spree.archive_admin_vendor_path(@vendor), { class: "dropdown-item", data: { turbo_method: :patch, turbo_confirm: Spree.t(:are_you_sure) } } %>
+ <% end %>
+
+<% end if can?(:edit, @vendor) && controller_name == 'vendors' %>
diff --git a/app/views/spree/admin/vendors/_onboarding_status.html.erb b/app/views/spree/admin/vendors/_onboarding_status.html.erb
new file mode 100644
index 00000000..2f4462ca
--- /dev/null
+++ b/app/views/spree/admin/vendors/_onboarding_status.html.erb
@@ -0,0 +1,45 @@
+
+
+
+
+ <% vendor.onboarding_tasks_list.each do |task| %>
+
+ <%= Spree.t("vendor_onboarding_tasks.#{task}") %>
+
+ <% if vendor.onboarding_task_done?(task) %>
+
+ <%= icon 'check', class: 'mr-1' %>
+ Done
+
+ <% else %>
+
+ <%= icon 'progress', class: 'mr-1' %>
+ Pending
+
+ <% end %>
+
+ <% end %>
+
+
+
diff --git a/app/views/spree/admin/vendors/_returns_address.html.erb b/app/views/spree/admin/vendors/_returns_address.html.erb
new file mode 100644
index 00000000..fb7e7879
--- /dev/null
+++ b/app/views/spree/admin/vendors/_returns_address.html.erb
@@ -0,0 +1,34 @@
+
+
+
+
+ <% if @vendor.returns_address.present? %>
+ <%= render 'spree/admin/shared/address', address: @vendor.returns_address %>
+
+ <%= render 'spree/admin/shared/map', map_object: @vendor.returns_address, label: @vendor.name %>
+ <% else %>
+
+ Not yet provided by partner
+
+ <% end %>
+
+
+
diff --git a/app/views/spree/admin/vendors/_stripe.html.erb b/app/views/spree/admin/vendors/_stripe.html.erb
new file mode 100644
index 00000000..5b56c7fe
--- /dev/null
+++ b/app/views/spree/admin/vendors/_stripe.html.erb
@@ -0,0 +1,59 @@
+<% if defined?(SpreeStripe::Gateway) %>
+
+
+
+
+ <% if @stripe_account.present? %>
+
Stripe account ID:
+
+
+ <%= text_field_tag :stripe_connect_id, @stripe_account.account_id, readonly: true, class: 'form-control-plaintext py-0 text-muted', data: { clipboard_target: 'source' } %>
+
+ <%= Spree.t(:copy) %>
+
+
+
+ <%= link_to "https://dashboard.stripe.com/connect/accounts/#{@stripe_account.account_id}/activity", class: 'btn btn-secondary btn-sm' do %>
+ Open Stripe dashboard
+ <%= icon('external-link', class: 'ml-2') %>
+ <% end %>
+
+
+
+
Payouts schedule:
+ <%= render 'display_payouts_schedule' %>
+ <% else %>
+
+ Not connected
+
+ <% end %>
+
+
+
+<% end%>
diff --git a/app/views/spree/admin/vendors/_table_filter_dropdown.html.erb b/app/views/spree/admin/vendors/_table_filter_dropdown.html.erb
new file mode 100644
index 00000000..aebfec67
--- /dev/null
+++ b/app/views/spree/admin/vendors/_table_filter_dropdown.html.erb
@@ -0,0 +1,38 @@
+
+
+
+ <%= Spree.t(:show) %>
+
+ <%= vendor_filter_dropdown_value %>
+
+
+
diff --git a/app/views/spree/admin/vendors/_tabs.html.erb b/app/views/spree/admin/vendors/_tabs.html.erb
index ac444a77..8b423c92 100644
--- a/app/views/spree/admin/vendors/_tabs.html.erb
+++ b/app/views/spree/admin/vendors/_tabs.html.erb
@@ -1,4 +1,104 @@
-<% content_for :page_title do %>
- <%= page_header_back_button spree.admin_vendors_path %>
- <%= @vendor.name %>
-<% end %>
+
+
+
+
+
+ <%= form_tag spree.admin_dashboard_analytics_path, method: :get, data: { turbo_frame: :analytics } do %>
+ <%= hidden_field_tag :vendor_id, vendor.id %>
+ <%= render 'spree/admin/shared/calendar_range_picker',
+ date_from_value: params[:date_from] || 1.month.ago.end_of_day,
+ date_to_value: params[:date_to] || Time.zone.now.beginning_of_day %>
+ <% end %>
+
+
+ <%= turbo_frame_tag :analytics, src: spree.admin_dashboard_analytics_path(vendor_id: vendor.id), loading: :lazy do %>
+ <%= render 'spree/admin/shared/analytics_loader_skeleton' %>
+ <% end %>
+
+
+
+ <%= turbo_frame_tag :products_list, src: spree.admin_vendor_products_path(vendor, frame_name: :products_list), loading: :lazy, autoscroll: true, data: { autoscroll_block: :nearest, autoscroll_behavior: :smooth } do %>
+ <%= render 'spree/admin/shared/spinner' %>
+ <% end %>
+
+
+
+
+ <%= turbo_frame_tag :orders_list, src: spree.admin_vendor_orders_path(vendor, frame_name: :orders_list), loading: :lazy, autoscroll: true, data: { autoscroll_block: :nearest, autoscroll_behavior: :smooth } do %>
+ <%= render 'spree/admin/shared/spinner' %>
+ <% end %>
+
+
+
+
+
+
+
+
+ <%= Spree.t(:user) %>
+ <%= Spree.t(:roles) %>
+ <%= Spree.t(:created_at) %>
+
+
+
+ <%= render partial: 'spree/admin/admin_users/admin_user', collection: @vendor_users %>
+
+
+
+
+
+
+
+
+
+
+
+ <%= Spree.t(:email) %>
+ <%= I18n.t('activerecord.attributes.spree/invitation.inviter') %>
+ <%= Spree.t(:roles) %>
+ <%= Spree.t(:status) %>
+ <%= Spree.t(:created_at) %>
+
+
+
+
+ <%= render collection: @vendor.invitations, partial: 'spree/admin/invitations/invitation', as: :invitation %>
+
+
+
+
+
+
+
+
+
+
+ <%= render 'spree/admin/policies/table_header' %>
+
+
+ <%= render partial: 'spree/admin/policies/table_row', collection: @vendor.policies, as: :policy %>
+
+
+
+
+
+
diff --git a/app/views/spree/admin/vendors/_totals.html.erb b/app/views/spree/admin/vendors/_totals.html.erb
new file mode 100644
index 00000000..c3c2b658
--- /dev/null
+++ b/app/views/spree/admin/vendors/_totals.html.erb
@@ -0,0 +1,43 @@
+
+
+
+ <%= render 'spree/admin/vendors/display_commission_rate', vendor: vendor %>
+
+
+ Products
+
+ <%= vendor.products.where.not(status: :archived).count %>
+
+
+
+ Total sales
+ <%= vendor.display_sales_total %>
+
+ <% if vendor.returns_address.present? %>
+
+ Country
+
+ <%= flag_emoji(vendor.returns_address.country_iso) %>
+ <%= vendor.returns_address.country_iso.upcase %>
+
+
+ <% end %>
+
+ <%= Spree.t(:created_at) %>
+ <%= local_time_ago(vendor.created_at, class: 'with-tip') %>
+
+
+
diff --git a/app/views/spree/admin/vendors/_vendor.html.erb b/app/views/spree/admin/vendors/_vendor.html.erb
new file mode 100644
index 00000000..29ab2269
--- /dev/null
+++ b/app/views/spree/admin/vendors/_vendor.html.erb
@@ -0,0 +1,51 @@
+
+
+ <%= vendor_logo_link(vendor, data: { row_link_target: 'link' }) %>
+
+
+ <%= vendor_status_badge(vendor) %>
+
+
+
+ <%= vendor.onboarding_tasks_done %> of <%= vendor.onboarding_tasks_total %> steps done
+
+
+
+
+ <% if vendor.respond_to?(:external_store) && vendor.external_store.present? %>
+ <% if vendor.external_store.shopify? %>
+ <%= image_tag 'shopify-icon.png', height: 20, alt: 'Shopify', title: 'Shopify', class: 'with-tip' %>
+ <% elsif vendor.external_store.woo_commerce? %>
+ <%= image_tag 'WooCommerce_logo.svg', height: 20, alt: 'Woo', title: 'Woo Commerce', class: 'with-tip' %>
+ <% end %>
+ <% else %>
+ <%= Spree.t(:not_available) %>
+ <% end %>
+
+
+ <% if vendor.products_count&.positive? %>
+ <%= vendor.products_count %>
+ <% else %>
+ <%= Spree.t(:not_available) %>
+ <% end %>
+
+
+ <% if vendor.sales_total.positive? %>
+ <%= vendor.display_sales_total %>
+ <% else %>
+ <%= Spree.t(:not_available) %>
+ <% end %>
+
+
+ <% if vendor.commission_total.positive? %>
+ <%= vendor.display_commission_total %>
+ <% else %>
+ <%= Spree.t(:not_available) %>
+ <% end %>
+
+
+ <%= local_time_ago(vendor.created_at, class: 'with-tip') %>
+
+
diff --git a/app/views/spree/admin/vendors/create.turbo_stream.erb b/app/views/spree/admin/vendors/create.turbo_stream.erb
new file mode 100644
index 00000000..90c38d22
--- /dev/null
+++ b/app/views/spree/admin/vendors/create.turbo_stream.erb
@@ -0,0 +1,2 @@
+<%= turbo_render_alerts %>
+<%= turbo_close_modal %>
diff --git a/app/views/spree/admin/vendors/edit.html.erb b/app/views/spree/admin/vendors/edit.html.erb
index d650d737..b089507e 100644
--- a/app/views/spree/admin/vendors/edit.html.erb
+++ b/app/views/spree/admin/vendors/edit.html.erb
@@ -1,8 +1,23 @@
-<%= render partial: 'spree/admin/vendors/tabs', locals: { current: :details } %>
+<% content_for :page_title do %>
+ <%= page_header_back_button spree.admin_vendor_path(@vendor) %>
+ <%= @vendor.name %>
+<% end %>
+
+<%= render partial: 'spree/admin/shared/error_messages', locals: { target: @vendor } %>
<%= form_for [:admin, @vendor] do |f| %>
-
- <%= render partial: 'form', locals: { f: f } %>
- <%= render partial: 'spree/admin/shared/edit_resource_links' %>
-
+
+
+ <%= f.label :name %>
+ <%= f.text_field :name, class: 'form-control', required: true, autofocus: true, placeholder: 'eg. Apple' %>
+
+
+ <%= f.label :billing_email %>
+ <%= f.email_field :billing_email, class: 'form-control', placeholder: 'eg. tim@apple.com' %>
+
+
+
+ <%= turbo_save_button_tag Spree.t('actions.update') %>
+ <%= link_to Spree.t('actions.discard'), spree.admin_vendor_path(@vendor), class: 'btn btn-light' %>
+
<% end %>
diff --git a/app/views/spree/admin/vendors/edit_billing_address.html.erb b/app/views/spree/admin/vendors/edit_billing_address.html.erb
new file mode 100644
index 00000000..84b6cb9d
--- /dev/null
+++ b/app/views/spree/admin/vendors/edit_billing_address.html.erb
@@ -0,0 +1,3 @@
+<%= turbo_frame_tag :edit_billing_address do %>
+ <%= render 'spree/admin/vendors/forms/billing_address', vendor: @vendor %>
+<% end %>
diff --git a/app/views/spree/admin/vendors/edit_brand.html.erb b/app/views/spree/admin/vendors/edit_brand.html.erb
new file mode 100644
index 00000000..3d74313f
--- /dev/null
+++ b/app/views/spree/admin/vendors/edit_brand.html.erb
@@ -0,0 +1,47 @@
+<%= turbo_frame_tag :dialog_modal_lg do %>
+ <%= form_for [:admin, @vendor] do |f| %>
+
+ <% end %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/spree/admin/vendors/edit_commission_rate.html.erb b/app/views/spree/admin/vendors/edit_commission_rate.html.erb
new file mode 100644
index 00000000..05642f65
--- /dev/null
+++ b/app/views/spree/admin/vendors/edit_commission_rate.html.erb
@@ -0,0 +1,22 @@
+<%= turbo_frame_tag "vendor_commision_rate_#{@vendor.id}" do %>
+
+ This will be the default commission rate for all products from this partner unless
+ you specify a different rate for a specific product.
+
+ <%= form_for [:admin, @vendor] do |f| %>
+
+
+ <%= turbo_save_button_tag %>
+ <%= link_to Spree.t('actions.discard'), spree.admin_vendor_path(@vendor, display_commission_rate: true), class: 'btn btn-light ml-2' %>
+
+ <% end %>
+<% end %>
diff --git a/app/views/spree/admin/vendors/edit_payouts_schedule.html.erb b/app/views/spree/admin/vendors/edit_payouts_schedule.html.erb
new file mode 100644
index 00000000..4e2b56a4
--- /dev/null
+++ b/app/views/spree/admin/vendors/edit_payouts_schedule.html.erb
@@ -0,0 +1,19 @@
+<%= turbo_frame_tag "vendor_payouts_schedule_#{@vendor.id}" do %>
+ <%= form_for [:admin, @vendor] do |f| %>
+
+
+ <%= turbo_save_button_tag %>
+ <%= link_to Spree.t('actions.discard'), spree.admin_vendor_path(@vendor, display_payouts_schedule: true), class: 'btn btn-light ml-2' %>
+
+ <% end %>
+<% end if current_store.respond_to?(:stripe_gateway) && current_store.stripe_gateway.present? && @vendor.respond_to?(:stripe_account) && @vendor.stripe_account.present? %>
diff --git a/app/views/spree/admin/vendors/edit_returns_address.html.erb b/app/views/spree/admin/vendors/edit_returns_address.html.erb
new file mode 100644
index 00000000..9e8cbc49
--- /dev/null
+++ b/app/views/spree/admin/vendors/edit_returns_address.html.erb
@@ -0,0 +1,3 @@
+<%= turbo_frame_tag :add_returns_address do %>
+ <%= render 'spree/admin/vendors/forms/returns_address', vendor: @vendor %>
+<% end %>
diff --git a/app/views/spree/admin/vendors/edit_returns_policy.html.erb b/app/views/spree/admin/vendors/edit_returns_policy.html.erb
new file mode 100644
index 00000000..22c20f79
--- /dev/null
+++ b/app/views/spree/admin/vendors/edit_returns_policy.html.erb
@@ -0,0 +1,34 @@
+<%= content_for(:page_title) do %>
+ <%= Spree.t(:returns) %>
+<% end %>
+
+
+
+
+
+
+ <%= turbo_frame_tag :add_policies do %>
+ <%= render 'spree/admin/vendors/forms/returns_policy', vendor: @vendor %>
+ <% end %>
+
+
+
+
+
+
+
+ <%= turbo_frame_tag :add_returns_address do %>
+ <%= render 'spree/admin/vendors/forms/returns_address', vendor: @vendor %>
+ <% end %>
+
+
+
+
\ No newline at end of file
diff --git a/app/views/spree/admin/vendors/forms/_billing_address.html.erb b/app/views/spree/admin/vendors/forms/_billing_address.html.erb
new file mode 100644
index 00000000..831af2eb
--- /dev/null
+++ b/app/views/spree/admin/vendors/forms/_billing_address.html.erb
@@ -0,0 +1,13 @@
+<%= form_for [:admin, vendor], url: spree.admin_vendor_path(vendor) do |f| %>
+ <% vendor.build_billing_address(country: current_store.default_country) unless vendor.billing_address %>
+ <%= f.fields_for :billing_address do |billing_address_form| %>
+ <%= render 'spree/addresses/form',
+ address_name: 'billing_address',
+ address_form: billing_address_form,
+ address_type: 'billing',
+ address: vendor.billing_address %>
+ <% end %>
+
+ <%= turbo_save_button_tag %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/spree/admin/vendors/forms/_operator_settings.html.erb b/app/views/spree/admin/vendors/forms/_operator_settings.html.erb
new file mode 100644
index 00000000..ccbc5596
--- /dev/null
+++ b/app/views/spree/admin/vendors/forms/_operator_settings.html.erb
@@ -0,0 +1,34 @@
+
+
+
<%= Spree.t('vendor.operator_settings') %>
+
+ This information isn't visible for Partners, only for the Operator.
+
+
+
+ <% if stripe_gateway.present? && @stripe_account.present? %>
+
+ <% end %>
+
+
+
diff --git a/app/views/spree/admin/vendors/forms/_returns_address.html.erb b/app/views/spree/admin/vendors/forms/_returns_address.html.erb
new file mode 100644
index 00000000..13d57777
--- /dev/null
+++ b/app/views/spree/admin/vendors/forms/_returns_address.html.erb
@@ -0,0 +1,13 @@
+<%= form_for [:admin, vendor], url: spree.admin_vendor_path(vendor) do |f| %>
+ <% vendor.build_returns_address(country: current_store.default_country) unless vendor.returns_address %>
+ <%= f.fields_for :returns_address do |returns_address_form| %>
+ <%= render 'spree/addresses/form',
+ address_name: 'returns_address',
+ address_form: returns_address_form,
+ address_type: 'shipping',
+ address: vendor.returns_address %>
+ <% end %>
+
+ <%= turbo_save_button_tag %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/spree/admin/vendors/forms/_returns_policy.html.erb b/app/views/spree/admin/vendors/forms/_returns_policy.html.erb
new file mode 100644
index 00000000..d4df773c
--- /dev/null
+++ b/app/views/spree/admin/vendors/forms/_returns_policy.html.erb
@@ -0,0 +1,10 @@
+<%= form_for [:admin, vendor], url: spree.admin_vendor_path(vendor) do |f| %>
+
+
+ <%= turbo_save_button_tag %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/spree/admin/vendors/index.csv.erb b/app/views/spree/admin/vendors/index.csv.erb
new file mode 100644
index 00000000..2777b0e8
--- /dev/null
+++ b/app/views/spree/admin/vendors/index.csv.erb
@@ -0,0 +1,46 @@
+<%- headers = [
+ 'Name',
+ 'Status',
+ 'Onboarding progress',
+ 'Integration',
+ 'Products (All)',
+ 'Live Products',
+ 'Revenue',
+ 'Commission (Rate)',
+ 'Commission (Total)',
+ 'Join Date',
+ 'Contact Person Email',
+ 'Billing name',
+ 'Billing email',
+ 'Returns Address Name',
+ 'Returns Address Street',
+ 'Returns Address City',
+ 'Returns Address Postal Code',
+ 'Returns Address State',
+ 'Returns Address Country'
+ ]
+-%>
+<%= CSV.generate_line(headers).strip %>
+<%- @vendors.each do |vendor| -%>
+<%= CSV.generate_line([
+ vendor.display_name,
+ vendor.state,
+ number_to_percentage(vendor.onboarding_percentage),
+ vendor.respond_to?(:external_store) ? vendor.external_store&.display_name : '',
+ vendor.products_count,
+ vendor.products.active.count,
+ vendor.sales_total,
+ number_to_percentage(vendor.platform_fee),
+ vendor.commission_total,
+ vendor.created_at,
+ vendor.contact_person_email,
+ vendor.billing_address&.company,
+ vendor.billing_email,
+ vendor.returns_address&.company,
+ vendor.returns_address&.street,
+ vendor.returns_address&.city,
+ vendor.returns_address&.zipcode,
+ vendor.returns_address&.state_text,
+ vendor.returns_address&.country_iso,
+ ]).strip.html_safe %>
+<%- end -%>
diff --git a/app/views/spree/admin/vendors/index.html.erb b/app/views/spree/admin/vendors/index.html.erb
index 6142a086..d7552dd4 100644
--- a/app/views/spree/admin/vendors/index.html.erb
+++ b/app/views/spree/admin/vendors/index.html.erb
@@ -1,77 +1,55 @@
<% content_for :page_title do %>
- <%= plural_resource_name(Spree::Vendor) %>
+ <%= Spree.t(:vendors) %>
<% end %>
<% content_for :page_actions do %>
- <%= button_link_to Spree.t(:new_vendor), new_object_url, { class: "btn-success", icon: 'add', id: 'admin_new_vendor' } %>
-<% end if can?(:create, Spree::Vendor) %>
+ <%= link_to spree.admin_vendors_path(q: params[:q].to_unsafe_hash, page: params[:page], per_page: params[:per_page], format: :csv), class: 'btn btn-light bg-transparent with-tip d-none d-lg-inline-flex', title: 'Export Vendors to a CSV file. To filter specific Vendors use "Filters" or status tabs before clicking this button' do %>
+ <%= icon 'table-export' %>
+ <%= Spree.t(:export) %>
+ <% end %>
-<% content_for :table_filter do %>
-
- <%= search_form_for [:admin, @search] do |f| %>
-
-
-
- <%= label_tag :q_name_cont, Spree.t(:name) %>
- <%= f.text_field :name_cont, tabindex: 1, class: "form-control js-quick-search-target" %>
-
-
-
-
- <%= label_tag :q_state_in, Spree.t(:state) %>
- <%= f.select :state_in, options_for_select(vendor_state_options), { multiple: 'please select' }, class: 'select2', multiple: true %>
-
-
-
-
- <%= button Spree.t(:filter_results), 'search' %>
-
- <% end %>
-
+ <%= invite_vendor_button if can?(:create, Spree::Vendor) %>
<% end %>
-<%= render partial: 'spree/admin/shared/index_table_options', locals: { collection: @collection } %>
+
+ <%= render "filters" %>
-<% if @vendors.any? %>
-
-
-
-
- <%= Spree.t(:user) %>
-
-
- <%= sort_link @search, :name, I18n.t(:name, scope: 'activerecord.attributes.spree/vendor') %>
-
-
- <%= sort_link @search, :state, I18n.t(:state, scope: 'activerecord.attributes.spree/vendor') %>
-
-
-
-
-
- <% @vendors.each do |vendor| %>
-
-
- <%= vendor.users.first.email rescue nil %>
-
-
- <%= vendor.name %>
-
-
- <%= vendor.state.humanize.downcase %>
-
-
-
- <%= link_to_edit vendor, no_text: true, class: 'edit' if can?(:edit, vendor) %>
-
-
- <% end %>
-
-
-<% else %>
-
- <%= Spree.t(:no_resource_found, resource: plural_resource_name(Spree::Vendor)) %>
-
-<% end %>
-
-<%= render partial: 'spree/admin/shared/index_table_options', locals: { collection: @collection } %>
+ <% if @vendors.any? %>
+
+
+
+
+ <%= sort_link @search, :name, I18n.t(:name, scope: "activerecord.attributes.spree/vendor") %>
+
+
+ <%= sort_link @search, :state, I18n.t(:state, scope: "activerecord.attributes.spree/vendor") %>
+
+
+ Onboarding progress
+
+
+ Integration
+
+
+ <%= sort_link @search, :products_count, Spree.t(:products_count) %>
+
+
+ <%= sort_link @search, :sales_total, Spree.t(:sales_total) %>
+
+
+ <%= sort_link @search, :commission_total, Spree.t(:commission_total) %>
+
+
+ <%= sort_link @search, :created_at, Spree.t(:created_at) %>
+
+
+
+ <%= render partial: "spree/admin/vendors/vendor", collection: @vendors, cached: spree_base_cache_scope %>
+
+
+
+ <% else %>
+ <%= render "spree/admin/shared/no_resource_found", new_object_url: nil %>
+ <% end %>
+ <%= render partial: "spree/admin/shared/index_table_options", locals: { collection: @collection } if @vendors.any? %>
+
diff --git a/app/views/spree/admin/vendors/new.html.erb b/app/views/spree/admin/vendors/new.html.erb
index d0cc461b..7d2e7a98 100644
--- a/app/views/spree/admin/vendors/new.html.erb
+++ b/app/views/spree/admin/vendors/new.html.erb
@@ -1,11 +1,50 @@
<% content_for :page_title do %>
- <%= link_to plural_resource_name(Spree::Vendor), spree.admin_vendors_url %> /
- <%= Spree.t(:new_vendor) %>
+ <%= page_header_back_button spree.admin_vendors_path %>
+ <%= Spree.t(:invite_vendor) %>
<% end %>
-<%= form_for [:admin, @vendor] do |f| %>
-
- <%= render partial: 'form', locals: { f: f } %>
- <%= render partial: 'spree/admin/shared/new_resource_links' %>
-
+<%= turbo_frame_tag :dialog_modal do %>
+ <%= form_for [:admin, @vendor] do |f| %>
+
+
+
+ <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @vendor } %>
+
+ <%= f.label :name, Spree.t('vendor_form.name') %>
+ <%= f.text_field :name, class: 'form-control', required: true, autofocus: true, placeholder: 'eg. Apple' %>
+
+
+ <%= f.label :contact_person_email do %>
+ <%= Spree.t('vendor_form.contact_person_email') %>
+ Better if it's a personal email
+ <% end %>
+ <%= f.email_field :contact_person_email, class: 'form-control', required: true, placeholder: 'eg. tim@apple.com' %>
+
+
+
+
+
+ <% end %>
<% end %>
diff --git a/app/views/spree/admin/vendors/show.html.erb b/app/views/spree/admin/vendors/show.html.erb
new file mode 100644
index 00000000..1ae5e2ea
--- /dev/null
+++ b/app/views/spree/admin/vendors/show.html.erb
@@ -0,0 +1,27 @@
+<% if turbo_frame_request? %>
+ <%= render 'spree/admin/vendors/display_commission_rate', vendor: @vendor if params[:display_commission_rate] %>
+ <%= render 'spree/admin/vendors/display_payouts_schedule', vendor: @vendor if params[:display_payouts_schedule] %>
+<% else %>
+ <%= render 'header' %>
+
+
+
+ <%= render 'spree/admin/vendors/card' %>
+ <%= render 'spree/admin/vendors/tabs', vendor: @vendor %>
+
+
+
+ <%= render 'spree/admin/vendors/onboarding_status', vendor: @vendor %>
+ <%= render 'spree/admin/vendors/totals', vendor: @vendor %>
+ <%= render 'spree/admin/vendors/stripe' %>
+ <%= render 'spree/admin/vendors/returns_address' %>
+ <%= render 'spree/admin/vendors/billing' %>
+
+
+<% end %>
+
+
diff --git a/app/views/spree/admin/vendors/translations.html.erb b/app/views/spree/admin/vendors/translations.html.erb
deleted file mode 100644
index ab8c9d03..00000000
--- a/app/views/spree/admin/vendors/translations.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<%= render partial: 'spree/admin/vendors/tabs', locals: { current: :translations } %>
-
-
- <%= render 'spree/admin/translations/form', resource: @object %>
-
diff --git a/app/views/spree/admin/vendors/update.turbo_stream.erb b/app/views/spree/admin/vendors/update.turbo_stream.erb
new file mode 100644
index 00000000..74dfcf26
--- /dev/null
+++ b/app/views/spree/admin/vendors/update.turbo_stream.erb
@@ -0,0 +1,23 @@
+<% @vendor.reload # reload associations %>
+
+<%= turbo_stream.replace "vendor_card_#{@vendor.id}" do %>
+ <%= render 'spree/admin/vendors/card', vendor: @vendor %>
+<% end %>
+
+<%= turbo_stream.replace "vendor_commision_rate_#{@vendor.id}" do %>
+ <%= render 'spree/admin/vendors/display_commission_rate', vendor: @vendor %>
+<% end %>
+
+<%= turbo_stream.replace "vendor_payouts_schedule_#{@vendor.id}" do %>
+ <%= render 'spree/admin/vendors/display_payouts_schedule', vendor: @vendor %>
+<% end %>
+
+<%= turbo_stream.replace :onboarding_tasks do %>
+ <%= turbo_frame_tag :onboarding_tasks do %>
+ <%= render 'spree/admin/dashboard/vendor_onboarding_progress', vendor: @vendor %>
+ <%= render 'spree/admin/dashboard/vendor_onboarding_tasks', vendor: @vendor %>
+ <% end %>
+<% end if current_vendor %>
+
+<%= turbo_render_alerts %>
+<%= turbo_close_modal %>
\ No newline at end of file
diff --git a/app/views/spree/api/v1/vendors/index.rabl b/app/views/spree/api/v1/vendors/index.rabl
deleted file mode 100644
index 3bf46a0c..00000000
--- a/app/views/spree/api/v1/vendors/index.rabl
+++ /dev/null
@@ -1,3 +0,0 @@
-collection @vendors
-
-extends 'spree/api/v1/vendors/show'
diff --git a/app/views/spree/api/v1/vendors/show.rabl b/app/views/spree/api/v1/vendors/show.rabl
deleted file mode 100644
index 770702da..00000000
--- a/app/views/spree/api/v1/vendors/show.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-object @vendor
-attributes *vendor_attributes
diff --git a/app/views/spree/order_mailer/splitted_order_confirm_email.html.erb b/app/views/spree/order_mailer/splitted_order_confirm_email.html.erb
new file mode 100644
index 00000000..b4d2bf45
--- /dev/null
+++ b/app/views/spree/order_mailer/splitted_order_confirm_email.html.erb
@@ -0,0 +1,25 @@
+
+ <%= Spree.t('order_mailer.confirm_email.dear_customer', name: name_for(@order)) %>
+
+
+ <%= Spree.t('order_mailer.confirm_email.instructions') %>
+
+<% @order.vendor_orders.each do |order| %>
+
+
+
+ <%= "#{Spree.t('order_mailer.confirm_email.order_summary', number: order.number)} - #{order.vendor.name}" %>
+
+
+
+
+ <%= render 'spree/shared/purchased_items_table', line_items: order.line_items, order: order %>
+
+
+
+<% end %>
+
+ <%= Spree.t('order_mailer.confirm_email.thanks') %>
+
+ <%= Spree.t('order_mailer.store_team', store_name: current_store.name) %>
+
diff --git a/app/views/spree/order_mailer/vendor_order_confirm_email.html.erb b/app/views/spree/order_mailer/vendor_order_confirm_email.html.erb
new file mode 100644
index 00000000..5ce54a7f
--- /dev/null
+++ b/app/views/spree/order_mailer/vendor_order_confirm_email.html.erb
@@ -0,0 +1,36 @@
+
+ <%= @order.name %> placed order <%= @order.number %> on <%= @order.completed_at.in_time_zone(@order.store.preferred_timezone).strftime('%b%e ') %> at <%= @order.completed_at.in_time_zone(@order.store.preferred_timezone).strftime('%l:%M %P') %>
+
+
+ View order
+
+
+
+
+
+
+
+ <%= render 'spree/shared/purchased_items_table', line_items: @order.line_items, order: @order %>
+
+
+
+
+
+
diff --git a/app/views/spree/page_sections/_brand_story.html.erb b/app/views/spree/page_sections/_brand_story.html.erb
new file mode 100644
index 00000000..94de4e1b
--- /dev/null
+++ b/app/views/spree/page_sections/_brand_story.html.erb
@@ -0,0 +1,51 @@
+<% cache_unless page_builder_enabled?, [*spree_base_cache_scope.call(section), product.brand].compact do %>
+ <% if product.brand && product.brand.image.attached? && product.brand.description.present? %>
+ <% logo = product.brand.square_image %>
+
+
+
+
+
+ <%= link_to spree.nested_taxons_path(product.brand) do %>
+ <%= spree_image_tag(product.brand.image, width: 770, height: 400, class: 'rounded', loading: :lazy, alt: product.brand.name) %>
+ <% end %>
+
+
+
+ <% if logo.attached? && logo.variable? %>
+ <%= link_to spree.nested_taxons_path(product.brand) do %>
+ <%= spree_image_tag(logo, width: 64, height: 64, class: 'rounded', loading: :lazy, alt: product.brand.name) %>
+ <% end %>
+ <% end %>
+
+
+ <%= product.brand.description %>
+
+
+
+ <%= link_to "#{Spree.t(:view)} #{product.brand.name}", spree.nested_taxons_path(product.brand), class: "link" %>
+
+
+
+
+
+
+ <% if logo.attached? && logo.variable? %>
+ <%= link_to spree.nested_taxons_path(product.brand) do %>
+ <%= spree_image_tag(logo, width: 64, height: 64, class: 'rounded', loading: :lazy, alt: product.brand.name) %>
+ <% end %>
+ <% end %>
+
+ <%= link_to spree.nested_taxons_path(product.brand) do %>
+ <%= spree_image_tag(product.brand.image, width: 360, height: 360, class: 'rounded', loading: :lazy, alt: product.brand.name) %>
+ <% end %>
+
+
+ <%= product.brand.description %>
+
+ <%= link_to "#{Spree.t(:view)} #{product.brand.name}", spree.nested_taxons_path(product.brand), class: "link" %>
+
+
+
+ <% end %>
+<% end %>
diff --git a/app/views/spree/vendor_mailer/vendor_approved.html.erb b/app/views/spree/vendor_mailer/vendor_approved.html.erb
new file mode 100644
index 00000000..b616cf75
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_approved.html.erb
@@ -0,0 +1,25 @@
+<% if @vendor.users.include?(params[:recipient]) %>
+
+ <%= Spree.t('vendor_mailer.greeting', vendor_name: @vendor.name) %>
+
+
+ <%= Spree.t('vendor_mailer.approved_email.message', vendor_name: @vendor.name, store_name: current_store.name).html_safe %>
+
+
+ <%= link_to 'Go to Dashboard', spree.admin_dashboard_url(host: @vendor.store.url) %>
+
+
+ <%= Spree.t('vendor_mailer.thanks') %>
+
+ <%= current_store.name %>
+
+<% else %>
+
Hi <%= params[:recipient].first_name %>!
+
+ <%= Spree.t('vendor_mailer.approved_email_to_marketplace_owners.message', vendor_name: @vendor.name, store_name: current_store.name).html_safe %>
+
+
+ <%= Spree.t('vendor_mailer.approved_email_to_marketplace_owners.link_to_profile_page',
+ profile_page: link_to('profile page', spree.admin_vendor_url(@vendor, host: current_store.url))).html_safe %>
+
+<% end %>
diff --git a/app/views/spree/vendor_mailer/vendor_approved.text.erb b/app/views/spree/vendor_mailer/vendor_approved.text.erb
new file mode 100644
index 00000000..8463f110
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_approved.text.erb
@@ -0,0 +1,8 @@
+<%= Spree.t('vendor_mailer.greeting', vendor_name: @vendor.name) %>
+
+<%= strip_tags Spree.t('vendor_mailer.approved_email.message', vendor_name: @vendor.name, store_name: current_store.name) %>
+Go to the dashboard by clicking on: <%= spree.admin_dashboard_url(host: @vendor.store.url) %>
+
+============================================================
+<%= Spree.t('vendor_mailer.thanks') %>
+<%= current_store.name %>
diff --git a/app/views/spree/vendor_mailer/vendor_notification_email.html.erb b/app/views/spree/vendor_mailer/vendor_notification_email.html.erb
deleted file mode 100644
index 8c7fcdfb..00000000
--- a/app/views/spree/vendor_mailer/vendor_notification_email.html.erb
+++ /dev/null
@@ -1,81 +0,0 @@
-
diff --git a/app/views/spree/vendor_mailer/vendor_notification_email.text.erb b/app/views/spree/vendor_mailer/vendor_notification_email.text.erb
deleted file mode 100644
index e9f15702..00000000
--- a/app/views/spree/vendor_mailer/vendor_notification_email.text.erb
+++ /dev/null
@@ -1,36 +0,0 @@
-<%= Spree.t('order_mailer.vendor_notification_email.dear_vendor') %>
-
-============================================================
-<%= Spree.t('order_mailer.vendor_notification_email.order_summary', number: @order.number) %>
-============================================================
-<% @line_items.each do |item| %>
- <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) <%= Spree.t('at_symbol') %> <%= item.single_money %> = <%= item.display_amount %>
-<% end %>
-============================================================
-<%= Spree.t('order_mailer.subtotal') %> <%= Spree::Money.new(@subtotal, currency: @order.currency) %>
-<% if @order.line_item_adjustments.exists? %>
- <% if @order.all_adjustments.promotion.eligible.exists? %>
- <% @order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %>
- <%= Spree.t(:promotion) %>: <%= label %> <%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %>
- <% end %>
- <% end %>
-<% end %>
-
-<% @order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %>
- <%= Spree.t(:shipping) %>: <%= name %> <%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @order.currency) %>
-<% end %>
-
-<% if @order.all_adjustments.eligible.tax.exists? %>
- <% @order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %>
- <%= Spree.t(:tax) %>: <%= label %> <%= Spree::Money.new(adjustments.sum(&:amount), currency: @order.currency) %>
- <% end %>
-<% end %>
-
-<% @order.adjustments.eligible.each do |adjustment| %>
- <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %>
- <%= adjustment.label %> <%= adjustment.display_amount %>
-<% end %>
-============================================================
-<%= Spree.t('order_mailer.total') %> <%= Spree::Money.new(@total, currency: @order.currency) %>
-
-<%= Spree.t('order_mailer.vendor_notification_email.thanks') %>
diff --git a/app/views/spree/vendor_mailer/vendor_onboarding_completed.html.erb b/app/views/spree/vendor_mailer/vendor_onboarding_completed.html.erb
new file mode 100644
index 00000000..41819904
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_onboarding_completed.html.erb
@@ -0,0 +1,6 @@
+
Hi <%= params[:recipient].first_name %>!
+
+ <%= Spree.t('vendor_mailer.onboarding_completed.message',
+ vendor_name: @vendor.name,
+ profile_page: link_to('profile page', spree.admin_vendor_url(@vendor, host: current_store.url))).html_safe %>
+
diff --git a/app/views/spree/vendor_mailer/vendor_onboarding_completed.text.erb b/app/views/spree/vendor_mailer/vendor_onboarding_completed.text.erb
new file mode 100644
index 00000000..0332b912
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_onboarding_completed.text.erb
@@ -0,0 +1,3 @@
+<%= strip_tags Spree.t('vendor_mailer.onboarding_completed.message',
+ vendor_name: @vendor.name,
+ profile_page: "profile page: #{spree.admin_vendor_url(@vendor, host: current_store.url)}") %>
diff --git a/app/views/spree/vendor_mailer/vendor_onboarding_started.html.erb b/app/views/spree/vendor_mailer/vendor_onboarding_started.html.erb
new file mode 100644
index 00000000..6c074b0e
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_onboarding_started.html.erb
@@ -0,0 +1,6 @@
+
Hi <%= params[:recipient].first_name %>!
+
+ <%= Spree.t('vendor_mailer.onboarding_started.message',
+ vendor_name: @vendor.name,
+ profile_page: link_to('profile page', spree.admin_vendor_url(@vendor, host: current_store.url))).html_safe %>
+
diff --git a/app/views/spree/vendor_mailer/vendor_onboarding_started.text.erb b/app/views/spree/vendor_mailer/vendor_onboarding_started.text.erb
new file mode 100644
index 00000000..bc217617
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_onboarding_started.text.erb
@@ -0,0 +1,3 @@
+<%= strip_tags Spree.t('vendor_mailer.onboarding_started.message',
+ vendor_name: @vendor.name,
+ profile_page: "profile page: #{spree.admin_vendor_url(@vendor, host: current_store.url)}") %>
diff --git a/app/views/spree/vendor_mailer/vendor_rejected.html.erb b/app/views/spree/vendor_mailer/vendor_rejected.html.erb
new file mode 100644
index 00000000..50f93084
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_rejected.html.erb
@@ -0,0 +1,11 @@
+
+ <%= Spree.t('vendor_mailer.greeting', vendor_name: @vendor.name) %>
+
+
+ <%= Spree.t('vendor_mailer.rejected_email.message', store_email: current_store.mail_from_address) %>
+
+
+ <%= Spree.t('vendor_mailer.thanks') %>
+
+ <%= current_store.name %>
+
diff --git a/app/views/spree/vendor_mailer/vendor_rejected.text.erb b/app/views/spree/vendor_mailer/vendor_rejected.text.erb
new file mode 100644
index 00000000..1f902eec
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_rejected.text.erb
@@ -0,0 +1,7 @@
+<%= Spree.t('vendor_mailer.greeting', vendor_name: @vendor.name) %>
+
+<%= Spree.t('vendor_mailer.rejected_email.message', store_email: current_store.mail_from_address) %>
+
+============================================================
+<%= Spree.t('vendor_mailer.thanks') %>
+<%= current_store.name %>
diff --git a/app/views/spree/vendor_mailer/vendor_suspended.html.erb b/app/views/spree/vendor_mailer/vendor_suspended.html.erb
new file mode 100644
index 00000000..8c593cd5
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_suspended.html.erb
@@ -0,0 +1,11 @@
+
+ <%= Spree.t('vendor_mailer.greeting', vendor_name: @vendor.name) %>
+
+
+ <%= Spree.t('vendor_mailer.suspended_email.message', store_email: current_store.mail_from_address) %>
+
+
+ <%= Spree.t('vendor_mailer.thanks') %>
+
+ <%= current_store.name %>
+
diff --git a/app/views/spree/vendor_mailer/vendor_suspended.text.erb b/app/views/spree/vendor_mailer/vendor_suspended.text.erb
new file mode 100644
index 00000000..64532e6a
--- /dev/null
+++ b/app/views/spree/vendor_mailer/vendor_suspended.text.erb
@@ -0,0 +1,7 @@
+<%= Spree.t('vendor_mailer.greeting', vendor_name: @vendor.name) %>
+
+<%= Spree.t('vendor_mailer.suspended_email.message', store_email: current_store.mail_from_address) %>
+
+============================================================
+<%= Spree.t('vendor_mailer.thanks') %>
+<%= current_store.name %>
diff --git a/app/views/spree_multi_vendor/_head.html.erb b/app/views/spree_multi_vendor/_head.html.erb
new file mode 100644
index 00000000..c49bd250
--- /dev/null
+++ b/app/views/spree_multi_vendor/_head.html.erb
@@ -0,0 +1 @@
+<%= javascript_import_module_tag 'application-spree-multi-vendor' %>
\ No newline at end of file
diff --git a/bin/importmap b/bin/importmap
new file mode 100755
index 00000000..bf720be9
--- /dev/null
+++ b/bin/importmap
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+
+require 'rails'
+
+require 'importmap-rails'
+require 'importmap/commands'
\ No newline at end of file
diff --git a/bin/rails b/bin/rails
old mode 100644
new mode 100755
index 2762b3ff..5806d5a5
--- a/bin/rails
+++ b/bin/rails
@@ -1,4 +1,5 @@
-# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" from the root of your extension
ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/spree_multi_vendor/engine', __FILE__)
diff --git a/config/importmap.rb b/config/importmap.rb
new file mode 100644
index 00000000..10f12833
--- /dev/null
+++ b/config/importmap.rb
@@ -0,0 +1,6 @@
+pin 'application-spree-multi-vendor', to: 'spree_multi_vendor/application.js', preload: false
+
+pin_all_from SpreeMultiVendor::Engine.root.join('app/javascript/spree_multi_vendor/controllers'),
+ under: 'spree_multi_vendor/controllers',
+ to: 'spree_multi_vendor/controllers',
+ preload: 'application-spree-multi-vendor'
diff --git a/config/initializers/admin_panel.rb b/config/initializers/admin_panel.rb
deleted file mode 100644
index e98ac664..00000000
--- a/config/initializers/admin_panel.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-Rails.application.config.after_initialize do
- if Spree::Core::Engine.backend_available?
- Rails.application.config.spree_backend.main_menu.insert_before('users', ::SpreeMultiVendor::Admin::MainMenu::OrganizationBuilder.new.build)
-
- Rails.application.config.spree_backend.tabs[:vendor] = ::SpreeMultiVendor::Admin::Tabs::VendorTabsBuilder.new.build
- end
-end
diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb
new file mode 100644
index 00000000..a19df280
--- /dev/null
+++ b/config/initializers/spree.rb
@@ -0,0 +1,191 @@
+Rails.application.config.after_initialize do
+ Spree.admin.partials.dashboard_sidebar << 'spree/admin/dashboard/vendors'
+ Spree.admin.partials.product_form_sidebar << 'spree/admin/products/form/platform_fee'
+
+ # Filters
+ Spree.admin.partials.orders_filters << 'spree/admin/orders/vendor_filter'
+ Spree.admin.partials.products_filters << 'spree/admin/products/vendor_filter'
+
+ # Order Page
+ Spree.admin.partials.order_page_body << 'spree/admin/orders/marketplace_suborders'
+ Spree.admin.partials.order_page_summary << 'spree/admin/orders/marketplace_summary'
+
+ # Shipping methods
+ Spree.admin.partials.shipping_methods_actions << 'spree/admin/shipping_methods/actions'
+
+ # Metafields
+ Spree.metafields.enabled_resources << Spree::Vendor
+
+ # Translations
+ Spree.translatable_resources << Spree::Vendor
+
+ # ===============================================
+ # Navigation for marketplace owners
+ # ===============================================
+
+ # Vendors
+ Spree.admin.navigation.sidebar.add :vendors,
+ label: :vendors,
+ url: :admin_vendors_path,
+ icon: 'heart-handshake',
+ position: 35,
+ active: -> { %w[vendors].include?(controller_name) },
+ if: -> { can?(:manage, current_store) } do |vendors|
+ # Vendors to review
+ vendors.add :vendors_to_review,
+ label: :vendors_to_review,
+ url: -> { spree.admin_vendors_path(q: { state_eq: 'ready_for_review' }) },
+ position: 10,
+ badge: -> { ready_for_review_vendors_count },
+ active: -> { %w[vendors].include?(controller_name) && params.dig(:q, :state_eq) == 'ready_for_review' },
+ if: -> { can?(:manage, current_store) && ready_for_review_vendors_count&.positive? }
+ end
+
+ # Marketplace settings
+ Spree.admin.navigation.settings.add :marketplace,
+ label: :marketplace,
+ url: :edit_admin_marketplace_settings_path,
+ icon: 'heart-handshake',
+ position: 51,
+ active: -> { %w[marketplace_settings].include?(controller_name) },
+ if: -> { can?(:manage, current_store) }
+
+ # ===============================================
+ # Navigation for vendors
+ # ===============================================
+ vendor_nav = Spree.admin.navigation.register_context(:vendor_sidebar)
+
+ # Dashboard / Getting Started
+ vendor_nav.add :getting_started,
+ label: 'admin.getting_started',
+ url: :admin_getting_started_path,
+ icon: 'map',
+ position: 5,
+ active: -> { controller_name == 'dashboard' && action_name == 'getting_started' },
+ if: -> { !current_vendor.onboarding_completed? },
+ badge: -> { "#{current_vendor.onboarding_tasks_done}/#{current_vendor.onboarding_tasks_total}" },
+ badge_class: 'badge-info'
+
+ # Dashboard / Home
+ vendor_nav.add :home,
+ label: :home,
+ url: :admin_path,
+ icon: 'home',
+ position: 10,
+ active: -> { controller_name == 'dashboard' && action_name == 'show' }
+
+ # Orders with submenu
+ vendor_nav.add :orders,
+ label: :orders,
+ url: :admin_orders_path,
+ icon: 'inbox',
+ position: 20,
+ if: -> { can?(:manage, Spree::Order) },
+ badge: -> { ready_to_ship_orders_count if ready_to_ship_orders_count&.positive? } do |orders|
+ # Orders to Fulfill submenu
+ orders.add :orders_to_fulfill,
+ label: 'admin.orders.orders_to_fulfill',
+ url: -> { spree.admin_orders_path(q: {shipment_state_not_in: [:shipped, :canceled]}) },
+ position: 10,
+ active: -> { controller_name == 'orders' && params.dig(:q, :shipment_state_not_in).present? },
+ if: -> { ready_to_ship_orders_count&.positive? },
+ badge: -> { ready_to_ship_orders_count if ready_to_ship_orders_count&.positive? }
+ end
+
+ # Products with submenu
+ vendor_nav.add :products,
+ label: :products,
+ url: :admin_products_path,
+ icon: 'package',
+ position: 30,
+ if: -> { can?(:manage, Spree::Product) } do |products|
+ # Stock
+ products.add :stock,
+ label: :stock,
+ url: :admin_stock_items_path,
+ position: 10,
+ active: -> { %w[stock_items stock_transfers].include?(controller_name) },
+ if: -> { can?(:manage, Spree::StockItem) || can?(:manage, Spree::StockTransfer) }
+ end
+
+ # Section divider before settings
+ vendor_nav.add :settings_section,
+ section_label: 'Settings',
+ position: 90
+
+ # Vendor settings (bottom of sidebar)
+ vendor_nav.add :settings,
+ label: :settings,
+ url: :edit_admin_vendor_settings_path,
+ icon: 'settings',
+ position: 100,
+ if: -> { can?(:manage, current_vendor) }
+
+ # Admin Users (bottom of sidebar)
+ vendor_nav.add :admin_users,
+ label: :users,
+ url: :admin_admin_users_path,
+ icon: 'users',
+ position: 110,
+ if: -> { can?(:manage, current_vendor) }
+
+ # ===============================================
+ # Vendor settings navigation
+ # ===============================================
+ vendor_settings_nav = Spree.admin.navigation.register_context(:vendor_settings)
+
+ # Vendor settings (bottom of sidebar)
+ vendor_settings_nav.add :settings,
+ label: :settings,
+ url: :edit_admin_vendor_settings_path,
+ icon: 'settings',
+ position: 10,
+ if: -> { can?(:manage, current_vendor) },
+ active: -> { %w[vendor_settings].include?(controller_name) }
+
+ # Admin Users
+ vendor_settings_nav.add :admin_users,
+ label: :users,
+ url: :admin_admin_users_path,
+ icon: 'users',
+ position: 20,
+ if: -> { can?(:manage, current_vendor) },
+ active: -> { %w[admin_users invitations].include?(controller_name) }
+
+ # Shipping methods
+ vendor_settings_nav.add :shipping_methods,
+ label: :shipping,
+ url: :admin_shipping_methods_path,
+ icon: 'truck',
+ position: 30,
+ if: -> { can?(:manage, Spree::ShippingMethod) },
+ active: -> { %w[shipping_methods].include?(controller_name) }
+
+ # Stock locations
+ vendor_settings_nav.add :stock_locations,
+ label: :stock_locations,
+ url: :admin_stock_locations_path,
+ icon: 'map-pin',
+ position: 40,
+ if: -> { can?(:manage, Spree::StockLocation) },
+ active: -> { %w[stock_locations].include?(controller_name) }
+
+ # Policies
+ vendor_settings_nav.add :policies,
+ label: :policies,
+ url: :admin_policies_path,
+ icon: 'list-check',
+ position: 50,
+ if: -> { can?(:manage, Spree::Policy) },
+ active: -> { %w[policies].include?(controller_name) }
+end
+
+module Spree
+ module PermittedAttributes
+ @@product_attributes += %i[platform_fee]
+ @@store_attributes += %i[
+ preferred_platform_fee
+ preferred_vendor_payouts_schedule_interval
+ ]
+ end
+end
diff --git a/config/initializers/spree_permitted_attributes.rb b/config/initializers/spree_permitted_attributes.rb
deleted file mode 100644
index 283a78e5..00000000
--- a/config/initializers/spree_permitted_attributes.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module Spree
- module PermittedAttributes
- ATTRIBUTES << :vendor_attributes
-
- mattr_reader *ATTRIBUTES
-
- @@vendor_attributes = [:name, :about_us, :contact_us, :notification_email]
- @@vendor_attributes << :image
- end
-end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ea1f71a5..ff096278 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,35 +1,80 @@
+---
en:
activerecord:
- models:
- spree/vendor:
- one: Vendor
- other: Vendors
attributes:
spree/vendor:
- about_us: About us
- contact_us: Contact us
- image: Image
- name: name
- state: state
- created_at: created at
- priority: priority
+ name: Name
+ state: Status
+ errors:
+ models:
+ spree/vendor:
+ attributes:
+ base:
+ cannot_destroy_vendor: Can't delete a partner with unfulfilled orders
+ contact_person_email:
+ cannot_be_store_admin: cannot belong to a store admin
spree:
- back_to_vendors_list: Back To Vendors List
- blocked: Blocked
- edit_vendor: Edit Vendor
- new_vendor: New Vendor
- only_alphanumeric_chars: should contain only letters and digits
- order_mailer:
- vendor_notification_email:
- dear_vendor: Dear Vendor,
- subject: Vendor notification
- thanks: Thanks
- vendor: Vendor
- vendor_name: Vendor Name
- vendor_states:
- active: Active
- blocked: Blocked
- pending: Pending
admin:
- commission: Commission
- vendors: Vendors
+ marketplace: Marketplace
+ vendors:
+ all: All Vendors
+ approved: Approved
+ invited: Invited
+ onboarding: Onboarding
+ pending: Pending
+ ready_for_review: Ready for Review
+ rejected: Rejected
+ suspended: Suspended
+ invite_vendor: Invite Vendor
+ payouts: Payouts
+ suborders: Suborders
+ transactions: Transactions
+ vendor_delivery_reimbursement_total: Vendor Shipping Reimbursement Total
+ vendor_form:
+ billing: Billing
+ billing_address: Billing Address
+ billing_email: Billing Email
+ contact_information: Contact Information
+ contact_person_email: Contact person email
+ name: Company or brand name
+ platform_fee: Commission rate
+ public_information: Public Information
+ settings: Settings
+ shopify_shop_name: Shopify Shop Name
+ vendor_mailer:
+ account: Account
+ approved_email:
+ message: 'Congrats
%{vendor_name} , you have been accepted
+ to %{store_name}!
You can now log in to your supplier dashboard:'
+ subject: "%{vendor_name} has been accepted to sell on %{store_name}"
+ approved_email_to_marketplace_owners:
+ link_to_profile_page: You can check their %{profile_page}
+ message: "%{vendor_name} has been approved to start selling on %{store_name}!"
+ greeting: Hey %{vendor_name},
+ onboarding_completed:
+ message: Good news, %{vendor_name} completed their onboarding process.
+ You can review their %{profile_page} and decide to approve %{vendor_name}
+ so they can start selling on your platform.
+ subject: "%{vendor_name} completed their onboarding process!"
+ onboarding_started:
+ message: Good news, %{vendor_name} accepted your invitation and started the
+ onboarding process.
You can track their progress on their %{profile_page}
+ subject: "%{vendor_name} just started the onboarding process!"
+ rejected_email:
+ message: Unfortunately we had to reject your application. If you would like
+ to discuss this matter please contact us via email at %{store_email}
+ subject: "%{vendor_name} has been rejected by %{store_name}"
+ suspended_email:
+ message: We're sorry but your organization was suspended. If you would like
+ to discuss this matter please contact us via email %{store_email}
+ subject: "%{vendor_name} has been suspended by %{store_name}"
+ thanks: Thank you,
+ vendor_onboarding_tasks:
+ add_billing_address: Add billing address
+ add_policies: Add policies
+ add_returns_address: Add returns address
+ map_shipping_methods: Map shipping methods
+ set_shipping_rates: Set shipping methods & rates
+ vendor_payouts_schedule_interval: Payouts schedule interval
+ vendor_reimbursement: Vendor Reimbursement
+ vendor_terms_of_service: Vendor Terms of Service
diff --git a/config/locales/es.yml b/config/locales/es.yml
deleted file mode 100644
index d7b30a5b..00000000
--- a/config/locales/es.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-es:
- activerecord:
- models:
- spree/vendor:
- one: Vendedor
- other: Vendedores
- attributes:
- spree/vendor:
- about_us: Acerca de
- commission_rate: Porcentaje Comisión
- contact_us: Contáctanos
- image: Imagen
- name: Nombre
- notification_email: Email notificación pedido
- state: estado
- created_at: creado en
- priority: prioridad
- spree:
- back_to_vendors_list: Volver a la lista de vendedores
- edit_vendor: Editar vendedor
- new_vendor: Nuevo vendedor
- only_alphanumeric_chars: DeberÃa contener solo letras y dÃgitos
- order_mailer:
- vendor_notification_email:
- dear_vendor: Estimado vendedor,
- order_summary: Resumen del pedido
- subject: Notificación de vendedor
- thanks: Gracias
- vendor: Vendedor
- vendor_name: Nombre del vendedor
- vendor_states:
- active: Activo
- blocked: Bloqueado
- pending: Pendiente
- admin:
- commission: Comisión
- vendors: Vendedores
\ No newline at end of file
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
deleted file mode 100644
index 4beed8db..00000000
--- a/config/locales/fr.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-fr:
- activerecord:
- models:
- spree/vendor:
- one: Vendeur
- other: Vendeurs
- attributes:
- spree/vendor:
- about_us: A propos
- contact_us: Contact
- image: Image
- name: Nom
- state: Status
- created_at: créé le
- priority: priorité
- spree:
- back_to_vendors_list: Retour à la liste des vendeurs
- edit_vendor: Modifier vendeur
- new_vendor: Nouveau vendeur
- only_alphanumeric_chars: ne doit contenir que des caractères alphanumériques
- order_mailer:
- vendor_notification_email:
- dear_vendor: Cher vendeur,
- subject: Notification vendeur
- thanks: Merci
- vendor: Vendeur
- vendor_name: Nom du vendeur
- vendor_states:
- active: Actif
- blocked: Bloqué
- pending: En attente
- admin:
- commission: Commission
- vendors: Vendeurs
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index e55b1bef..5926a23f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,27 +1,53 @@
-Spree::Core::Engine.routes.draw do
- namespace :admin do
+Spree::Core::Engine.add_routes do
+ # admin panel routes
+ namespace :admin, path: Spree.admin_path do
resources :vendors do
+ collection do
+ get :select_options, defaults: { format: :json }
+ end
member do
- get :translations
- post :translations, to: 'vendors#edit_translations'
+ get :edit_commission_rate
+ get :edit_payouts_schedule
+ get :edit_returns_address
+ get :edit_billing_address
+ get :edit_returns_policy
+ get :edit_brand
+ patch :approve
+ patch :reject
+ patch :suspend
+ patch :archive
+ patch :confirm_shipping_rates
+ patch :use_no_external_store
end
+ resources :products, only: %i[index]
+ resources :orders, only: [:index]
- collection do
- post :update_positions
- end
+ resources :role_users, only: [:destroy]
end
- get 'vendor_settings' => 'vendor_settings#edit'
- patch 'vendor_settings' => 'vendor_settings#update'
+ resource :vendor_settings, only: [:edit, :update], controller: 'vendor_settings'
+
+ resource :marketplace_settings, only: [:edit, :update], controller: 'marketplace_settings'
end
+ # api routes
namespace :api, defaults: { format: 'json' } do
namespace :v2 do
+ namespace :platform do
+ resources :vendors do
+ member do
+ patch :invite
+ patch :start_onboarding
+ patch :complete_onboarding
+ patch :approve
+ patch :reject
+ patch :suspend
+ end
+ end
+ end
+
namespace :storefront do
- resources :vendors, only: [:show,:index]
+ resources :vendors, only: [:show, :index]
end
end
- namespace :v1 do
- resources :vendors
- end
end
end
diff --git a/db/default/vendors.rb b/db/default/vendors.rb
deleted file mode 100644
index d18cb515..00000000
--- a/db/default/vendors.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-vendor = Spree::Vendor.first_or_initialize do |v|
- v.name = 'Test Vendor'
-end
-vendor.save!
-
-puts "Created Vendor with the name \"#{vendor.name}\"!"
-
-user = Spree.user_class.where(email: 'user@vendor.com').first_or_initialize do |u|
- u.password = u.password_confirmation = 'vendor123'
-end
-
-vendor.users << user unless vendor.users.include?(user)
-puts "Created Vendor Admin User with an email \"#{user.email}\" and password \"#{user.password}\"!"
\ No newline at end of file
diff --git a/db/migrate/20170406102250_create_spree_vendors.rb b/db/migrate/20170406102250_create_spree_vendors.rb
deleted file mode 100644
index 4f347c5f..00000000
--- a/db/migrate/20170406102250_create_spree_vendors.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class CreateSpreeVendors < SpreeExtension::Migration[4.2]
- def change
- create_table :spree_vendors do |t|
- t.string :name
- t.timestamps
- end
- add_index :spree_vendors, :name, unique: true
- end
-end
diff --git a/db/migrate/20170406102944_add_vendor_id_to_spree_models.rb b/db/migrate/20170406102944_add_vendor_id_to_spree_models.rb
deleted file mode 100644
index f3fa3185..00000000
--- a/db/migrate/20170406102944_add_vendor_id_to_spree_models.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class AddVendorIdToSpreeModels < SpreeExtension::Migration[4.2]
- def change
- SpreeMultiVendor.vendorized_models.each do |klass|
- add_reference klass.table_name, :vendor, index: true
- rescue
- message = "Could not add vendor_id column to spree_#{klass}"
- Rails.logger.error message
- end
- end
-end
diff --git a/db/migrate/20170410111150_add_state_to_vendors.rb b/db/migrate/20170410111150_add_state_to_vendors.rb
deleted file mode 100644
index 04cc5d48..00000000
--- a/db/migrate/20170410111150_add_state_to_vendors.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class AddStateToVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :state, :string
- add_index :spree_vendors, :state
- end
-end
diff --git a/db/migrate/20170412124925_add_deleted_at_to_spree_vendors.rb b/db/migrate/20170412124925_add_deleted_at_to_spree_vendors.rb
deleted file mode 100644
index 978b4852..00000000
--- a/db/migrate/20170412124925_add_deleted_at_to_spree_vendors.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class AddDeletedAtToSpreeVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :deleted_at, :datetime
- add_index :spree_vendors, :deleted_at
- end
-end
diff --git a/db/migrate/20170413094447_create_spree_vendor_users.rb b/db/migrate/20170413094447_create_spree_vendor_users.rb
deleted file mode 100644
index a00514c8..00000000
--- a/db/migrate/20170413094447_create_spree_vendor_users.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CreateSpreeVendorUsers < SpreeExtension::Migration[4.2]
- def change
- create_table :spree_vendor_users do |t|
- t.references :vendor
- t.references :user
- end
- add_index :spree_vendor_users, [:vendor_id, :user_id], unique: true
- add_index :spree_vendor_users, :vendor_id
- add_index :spree_vendor_users, :user_id
- end
-end
diff --git a/db/migrate/20190212161426_add_slug_to_spree_vendors.rb b/db/migrate/20190212161426_add_slug_to_spree_vendors.rb
deleted file mode 100644
index a3403cc2..00000000
--- a/db/migrate/20190212161426_add_slug_to_spree_vendors.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class AddSlugToSpreeVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :slug, :string
- add_index :spree_vendors, :slug, unique: true
- end
-end
diff --git a/db/migrate/20190214120226_add_about_us_to_spree_vendors.rb b/db/migrate/20190214120226_add_about_us_to_spree_vendors.rb
deleted file mode 100644
index a3bd77fa..00000000
--- a/db/migrate/20190214120226_add_about_us_to_spree_vendors.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddAboutUsToSpreeVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :about_us, :text
- end
-end
diff --git a/db/migrate/20190214142526_add_contact_us_to_spree_vendors.rb b/db/migrate/20190214142526_add_contact_us_to_spree_vendors.rb
deleted file mode 100644
index 3b9f79e3..00000000
--- a/db/migrate/20190214142526_add_contact_us_to_spree_vendors.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddContactUsToSpreeVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :contact_us, :text
- end
-end
diff --git a/db/migrate/20190305120337_add_commission_to_spree_vendors.rb b/db/migrate/20190305120337_add_commission_to_spree_vendors.rb
deleted file mode 100644
index 672ec426..00000000
--- a/db/migrate/20190305120337_add_commission_to_spree_vendors.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddCommissionToSpreeVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :commission_rate, :float, default: '5.0'
- end
-end
diff --git a/db/migrate/20190308091546_add_priority_to_vendor.rb b/db/migrate/20190308091546_add_priority_to_vendor.rb
deleted file mode 100644
index d10945ea..00000000
--- a/db/migrate/20190308091546_add_priority_to_vendor.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class AddPriorityToVendor < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :priority, :integer
- Spree::Vendor.order(:updated_at).each.with_index(1) do |vendor, index|
- vendor.update_column :priority, index
- end
- end
-end
diff --git a/db/migrate/20190312130754_create_spree_order_commissions.rb b/db/migrate/20190312130754_create_spree_order_commissions.rb
deleted file mode 100644
index 769a4048..00000000
--- a/db/migrate/20190312130754_create_spree_order_commissions.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class CreateSpreeOrderCommissions < SpreeExtension::Migration[4.2]
- def change
- create_table :spree_order_commissions do |t|
- t.references :order
- t.references :vendor
- t.float :amount
-
- t.timestamps
- end
- add_index :spree_order_commissions, [:order_id, :vendor_id], unique: true
- add_index :spree_order_commissions, :order_id
- add_index :spree_order_commissions, :vendor_id
- end
-end
diff --git a/db/migrate/20190322133128_add_notification_email_to_vendors.rb b/db/migrate/20190322133128_add_notification_email_to_vendors.rb
deleted file mode 100644
index 3ba31f13..00000000
--- a/db/migrate/20190322133128_add_notification_email_to_vendors.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddNotificationEmailToVendors < SpreeExtension::Migration[4.2]
- def change
- add_column :spree_vendors, :notification_email, :string
- end
-end
diff --git a/db/migrate/20200416030721_add_translations_to_vendor.rb b/db/migrate/20200416030721_add_translations_to_vendor.rb
deleted file mode 100644
index 90b48e18..00000000
--- a/db/migrate/20200416030721_add_translations_to_vendor.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-# Based on issue https://github.com/spree-contrib/spree_multi_vendor/issues/104
-# This is a patch for Spree globalize to be friendly with the gem
-
-class AddTranslationsToVendor < ActiveRecord::Migration[4.2]
- def up
- params = { name: :string, about_us: :text, contact_us: :text, slug: :string }
- if defined?(SpreeGlobalize)
- Spree::Vendor.create_translation_table!(params, { migrate_data: true })
- end
- end
-
- def down
- if defined?(SpreeGlobalize)
- Spree::Vendor.drop_translation_table! migrate_data: true
- end
- end
-end
diff --git a/db/migrate/20210728120649_change_vendorized_models_vendor_id_type.rb b/db/migrate/20210728120649_change_vendorized_models_vendor_id_type.rb
deleted file mode 100644
index 628b282b..00000000
--- a/db/migrate/20210728120649_change_vendorized_models_vendor_id_type.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class ChangeVendorizedModelsVendorIdType < ActiveRecord::Migration[4.2]
- def change
- SpreeMultiVendor.vendorized_models.each do |klass|
- change_column klass.table_name, :vendor_id, :bigint
- rescue
- message = "Could not change vendor_id column for spree_#{klass}"
- Rails.logger.error message
- end
- end
-end
diff --git a/db/migrate/20210728120910_change_vendor_id_and_user_id_type_for_spree_vendor_users.rb b/db/migrate/20210728120910_change_vendor_id_and_user_id_type_for_spree_vendor_users.rb
deleted file mode 100644
index 651f6180..00000000
--- a/db/migrate/20210728120910_change_vendor_id_and_user_id_type_for_spree_vendor_users.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class ChangeVendorIdAndUserIdTypeForSpreeVendorUsers < ActiveRecord::Migration[4.2]
- def up
- change_table(:spree_vendor_users) do |t|
- t.change :vendor_id, :bigint
- t.change :user_id, :bigint
- end
- end
- def down
- change_table(:spree_vendor_users) do |t|
- t.change :vendor_id, :integer
- t.change :user_id, :integer
- end
- end
-end
diff --git a/db/migrate/20210728121024_change_vendor_id_and_order_id_type_for_spree_order_commissions.rb b/db/migrate/20210728121024_change_vendor_id_and_order_id_type_for_spree_order_commissions.rb
deleted file mode 100644
index 35330bd8..00000000
--- a/db/migrate/20210728121024_change_vendor_id_and_order_id_type_for_spree_order_commissions.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class ChangeVendorIdAndOrderIdTypeForSpreeOrderCommissions < ActiveRecord::Migration[4.2]
- def up
- change_table(:spree_order_commissions) do |t|
- t.change :vendor_id, :bigint
- t.change :order_id, :bigint
- end
- end
- def down
- change_table(:spree_order_commissions) do |t|
- t.change :vendor_id, :integer
- t.change :order_id, :integer
- end
- end
-end
diff --git a/db/migrate/20231114090744_add_mobility_translations_to_vendor.rb b/db/migrate/20231114090744_add_mobility_translations_to_vendor.rb
deleted file mode 100644
index ff09ed90..00000000
--- a/db/migrate/20231114090744_add_mobility_translations_to_vendor.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class AddMobilityTranslationsToVendor < ActiveRecord::Migration[6.1]
- def change
- if ActiveRecord::Base.connection.table_exists?('spree_vendor_translations')
- if ActiveRecord::Migration.connection.index_exists?(:spree_vendors_translations, :spree_vendor_id)
- remove_index :spree_vendor_translations, column: :spree_vendor_id, if_exists: true
- end
- else
- create_table :spree_vendor_translations do |t|
- # Translated attribute(s)
- t.string :name
- t.text :about_us
- t.text :contact_us
- t.string :slug
-
- t.string :locale, null: false
- t.references :spree_vendor, null: false, foreign_key: true, index: false
-
- t.timestamps null: false
- end
-
- add_index :spree_vendor_translations, :locale, name: :index_spree_vendor_translations_on_locale
- add_index :spree_vendor_translations, [:locale, :slug], unique: true, name: 'vendor_unique_slug_per_locale'
- end
- end
-end
diff --git a/db/migrate/20231114093444_transfer_vendor_data_to_translatable_tables.rb b/db/migrate/20231114093444_transfer_vendor_data_to_translatable_tables.rb
deleted file mode 100644
index 9fdf74a0..00000000
--- a/db/migrate/20231114093444_transfer_vendor_data_to_translatable_tables.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class TransferVendorDataToTranslatableTables < ActiveRecord::Migration[6.1]
- TRANSLATION_MIGRATION = Spree::TranslationMigrations.new(Spree::Vendor, 'en')
-
- def up
- TRANSLATION_MIGRATION.transfer_translation_data
- end
-
- def down
- TRANSLATION_MIGRATION.revert_translation_data_transfer
- end
-end
diff --git a/db/migrate/20250624185218_setup_spree_multi_vendor_open_source.rb b/db/migrate/20250624185218_setup_spree_multi_vendor_open_source.rb
new file mode 100644
index 00000000..0c1b77cf
--- /dev/null
+++ b/db/migrate/20250624185218_setup_spree_multi_vendor_open_source.rb
@@ -0,0 +1,106 @@
+class SetupSpreeMultiVendorOpenSource < ActiveRecord::Migration[7.2]
+ def change
+ create_table :spree_vendors, if_not_exists: true do |t|
+ t.string :name, index: true, null: false
+ t.string :original_name, index: true
+ t.string :state, index: true, null: false
+
+ if t.respond_to? :jsonb
+ t.jsonb :public_metadata
+ t.jsonb :private_metadata
+ else
+ t.json :public_metadata
+ t.json :private_metadata
+ end
+
+ t.integer :products_count, default: 0, null: false, index: true
+ t.decimal :sales_total, precision: 8, scale: 2, default: "0.0", null: false, index: true
+ t.decimal :commission_total, precision: 8, scale: 2, default: "0.0", null: false, index: true
+
+ t.bigint :returns_address_id, index: true
+ t.string :contact_person_email, index: true
+ t.bigint :invited_by_id, index: true
+ t.bigint :billing_address_id, index: true
+ t.string :billing_email
+
+ t.text :preferences
+ t.decimal :platform_fee, precision: 10, scale: 2, default: 30.0, null: false
+ t.string :payouts_schedule_interval, default: 'monthly', null: false
+
+ t.datetime :deleted_at, index: true
+ t.timestamps
+ end
+
+ create_table :spree_external_categories, if_not_exists: true do |t|
+ t.string :name, null: false, index: true
+ t.bigint :taxon_id, index: true
+ t.bigint :vendor_id, index: true
+
+ t.string :external_id, index: true
+ t.string :type, null: false, index: true
+
+ t.datetime :deleted_at, index: true
+ t.timestamps
+ end
+
+ create_table :spree_platform_fees, if_not_exists: true do |t|
+ t.bigint :order_id, null: false, index: true
+ t.string :feeable_type, null: false
+ t.bigint :feeable_id, null: false
+ t.string :label, index: true
+ t.boolean :active, default: true, index: true
+ t.decimal :amount, precision: 10, scale: 2, null: false
+ t.decimal :rate, precision: 5, scale: 2
+
+ t.timestamps
+ end
+
+ add_index :spree_platform_fees, %w[feeable_id feeable_type], name: "index_spree_platform_fees_on_feeable_id_and_feeable_type", if_not_exists: true
+
+ %w[
+ spree_line_items
+ spree_orders
+ spree_payments
+ spree_products
+ spree_reports
+ spree_shipments
+ spree_shipping_methods
+ spree_stock_locations
+ spree_webhooks_subscribers
+ ].each do |table|
+ if table_exists?(table)
+ add_column table, :vendor_id, :bigint, if_not_exists: true
+ add_index table, :vendor_id, if_not_exists: true
+ end
+ end
+
+ %w[
+ spree_product_properties
+ spree_products
+ spree_shipments
+ spree_shipping_methods
+ spree_stock_items
+ spree_variants
+ spree_assets
+ spree_orders
+ spree_line_items
+ ].each do |table|
+ if table_exists?(table)
+ add_column table, :external_id, :string, if_not_exists: true
+ add_index table, :external_id, if_not_exists: true
+ end
+ end
+
+ add_column :spree_products, :platform_fee, :decimal, precision: 10, scale: 2, null: true, if_not_exists: true
+
+ add_column :spree_orders, :platform_fee_total, :decimal, precision: 10, scale: 2, default: "0.0", if_not_exists: true
+ add_column :spree_orders, :parent_id, :bigint, if_not_exists: true
+ add_index :spree_orders, :parent_id, if_not_exists: true
+ add_column :spree_orders, :platform_fee_reverse_total, :decimal, precision: 10, scale: 2, default: "0.0", if_not_exists: true
+
+ add_column :spree_line_items, :platform_fee_total, :decimal, precision: 10, scale: 2, default: "0.0", if_not_exists: true
+ add_column :spree_line_items, :platform_fee_rate, :decimal, precision: 5, scale: 2, if_not_exists: true
+ add_column :spree_line_items, :platform_fee_per_unit, :decimal, precision: 10, scale: 2, default: "0.0", if_not_exists: true
+ add_column :spree_line_items, :platform_fee_reverse_total, :decimal, precision: 10, scale: 2, default: "0.0", if_not_exists: true
+ end
+end
diff --git a/db/migrate/20250729094648_add_slug_to_spree_vendors.rb b/db/migrate/20250729094648_add_slug_to_spree_vendors.rb
new file mode 100644
index 00000000..9dda0987
--- /dev/null
+++ b/db/migrate/20250729094648_add_slug_to_spree_vendors.rb
@@ -0,0 +1,6 @@
+class AddSlugToSpreeVendors < ActiveRecord::Migration[7.2]
+ def change
+ add_column :spree_vendors, :slug, :string, if_not_exists: true
+ add_index :spree_vendors, :slug, unique: true, if_not_exists: true
+ end
+end
diff --git a/db/migrate/20250731115051_add_vendor_id_to_spree_exports.rb b/db/migrate/20250731115051_add_vendor_id_to_spree_exports.rb
new file mode 100644
index 00000000..b17798a5
--- /dev/null
+++ b/db/migrate/20250731115051_add_vendor_id_to_spree_exports.rb
@@ -0,0 +1,7 @@
+class AddVendorIdToSpreeExports < ActiveRecord::Migration[7.2]
+ def change
+ unless column_exists?(:spree_exports, :vendor_id)
+ add_reference :spree_exports, :vendor
+ end
+ end
+end
diff --git a/db/migrate/20251112115854_create_spree_vendor_translations.rb b/db/migrate/20251112115854_create_spree_vendor_translations.rb
new file mode 100644
index 00000000..c56bc152
--- /dev/null
+++ b/db/migrate/20251112115854_create_spree_vendor_translations.rb
@@ -0,0 +1,12 @@
+class CreateSpreeVendorTranslations < ActiveRecord::Migration[7.2]
+ def change
+ create_table :spree_vendor_translations do |t|
+ t.string :locale, null: false
+ t.references :spree_vendor, null: false
+
+ t.timestamps
+ end
+
+ add_index :spree_vendor_translations, %i[spree_vendor_id locale], unique: true
+ end
+end
diff --git a/lib/generators/spree_multi_vendor/install/install_generator.rb b/lib/generators/spree_multi_vendor/install/install_generator.rb
index fb46267d..0a6a186a 100644
--- a/lib/generators/spree_multi_vendor/install/install_generator.rb
+++ b/lib/generators/spree_multi_vendor/install/install_generator.rb
@@ -1,19 +1,24 @@
module SpreeMultiVendor
module Generators
class InstallGenerator < Rails::Generators::Base
+ class_option :migrate, type: :boolean, default: true
- class_option :auto_run_migrations, type: :boolean, default: false
+ source_root File.expand_path('templates', __dir__)
+
+ def copy_initializer
+ template 'config/initializers/spree_multi_vendor.rb', 'config/initializers/spree_multi_vendor.rb'
+ end
def add_migrations
run 'bundle exec rake railties:install:migrations FROM=spree_multi_vendor'
end
def run_migrations
- run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
+ run_migrations = options[:migrate] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
if run_migrations
- run 'bundle exec rake db:migrate'
+ run 'bin/rails db:migrate'
else
- puts 'Skipping rake db:migrate, don\'t forget to run it!'
+ puts 'Skipping rails db:migrate, don\'t forget to run it!'
end
end
end
diff --git a/lib/generators/spree_multi_vendor/install/templates/config/initializers/spree_multi_vendor.rb b/lib/generators/spree_multi_vendor/install/templates/config/initializers/spree_multi_vendor.rb
new file mode 100644
index 00000000..9b3be028
--- /dev/null
+++ b/lib/generators/spree_multi_vendor/install/templates/config/initializers/spree_multi_vendor.rb
@@ -0,0 +1,4 @@
+Rails.application.config.after_initialize do
+ Spree.permissions.assign(:admin, SpreeMultiVendor::PermissionSets::SuperUser)
+ Spree.permissions.assign(:vendor, SpreeMultiVendor::PermissionSets::VendorUser)
+end
diff --git a/lib/generators/spree_multi_vendor/mailers_preview/mailers_preview_generator.rb b/lib/generators/spree_multi_vendor/mailers_preview/mailers_preview_generator.rb
deleted file mode 100644
index 7bf86532..00000000
--- a/lib/generators/spree_multi_vendor/mailers_preview/mailers_preview_generator.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'rails/generators'
-require 'spree/core'
-
-module SpreeMultiVendor
- module Generators
- class MailersPreviewGenerator < Rails::Generators::Base
- desc 'Generates mailers preview for development proposes'
-
- def self.source_paths
- [
- File.expand_path('templates', __dir__)
- ]
- end
-
- def copy_mailers_previews
- preview_path = Rails.application.config.action_mailer.preview_path || 'test/mailers/previews'
-
- template 'mailers/previews/vendor_notification_preview.rb', "#{preview_path}/vendor_notification_preview.rb"
- end
- end
- end
-end
diff --git a/lib/generators/spree_multi_vendor/mailers_preview/templates/mailers/previews/vendor_notification_preview.rb b/lib/generators/spree_multi_vendor/mailers_preview/templates/mailers/previews/vendor_notification_preview.rb
deleted file mode 100644
index 7acae268..00000000
--- a/lib/generators/spree_multi_vendor/mailers_preview/templates/mailers/previews/vendor_notification_preview.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class VendorNotificationPreview < ActionMailer::Preview
- def vendor_notification_email
- Spree::VendorMailer.vendor_notification_email(Spree::Order.complete.first.id, Spree::Vendor.first.id)
- end
-end
diff --git a/lib/spree_multi_vendor.rb b/lib/spree_multi_vendor.rb
index 08f5988a..f0ed14be 100644
--- a/lib/spree_multi_vendor.rb
+++ b/lib/spree_multi_vendor.rb
@@ -1,16 +1,13 @@
require 'spree_core'
-require 'spree_backend'
-require 'spree_emails'
+require 'spree_extension'
require 'spree_multi_vendor/engine'
require 'spree_multi_vendor/version'
-require 'spree_extension'
-require 'deface'
+require 'spree_multi_vendor/configuration'
module SpreeMultiVendor
- # TODO: this should be moved into preferences
- def self.vendorized_models
- SpreeMultiVendor::Config[:vendorized_models].map(&:classify).map do |class_name|
- "::Spree::#{class_name}".safe_constantize
- end.compact.uniq
+ mattr_accessor :queue
+
+ def self.queue
+ @@queue ||= Spree.queues.default
end
end
diff --git a/lib/spree_multi_vendor/configuration.rb b/lib/spree_multi_vendor/configuration.rb
index 980c0519..8b908eec 100644
--- a/lib/spree_multi_vendor/configuration.rb
+++ b/lib/spree_multi_vendor/configuration.rb
@@ -1,27 +1,6 @@
module SpreeMultiVendor
- class Configuration
- DEFAULT_VENDORIZED_MODELS ||= %w[product variant stock_location shipping_method].freeze
-
- attr_accessor :vendorized_models
-
- def initialize
- self.vendorized_models = DEFAULT_VENDORIZED_MODELS
- end
-
- def configure
- yield(self) if block_given?
- end
-
- def get(preference)
- send(preference)
- end
-
- alias [] get
-
- def set(preference, value)
- send("#{preference}=", value)
- end
-
- alias []= set
+ class Configuration < Spree::Preferences::Configuration
+ preference :shopify_app_name, :string, default: 'Vendo Connect'
+ preference :default_policies, :array, default: ['returns_policy']
end
end
diff --git a/lib/spree_multi_vendor/engine.rb b/lib/spree_multi_vendor/engine.rb
index 8dba0398..4b3d101a 100644
--- a/lib/spree_multi_vendor/engine.rb
+++ b/lib/spree_multi_vendor/engine.rb
@@ -1,5 +1,3 @@
-require_relative 'configuration'
-
module SpreeMultiVendor
class Engine < Rails::Engine
require 'spree/core'
@@ -15,8 +13,12 @@ class Engine < Rails::Engine
SpreeMultiVendor::Config = SpreeMultiVendor::Configuration.new
end
- config.after_initialize do
- ::Spree::PermittedAttributes.product_attributes << :vendor_id if SpreeMultiVendor::Config[:vendorized_models].include?('product')
+ initializer 'spree_multi_vendor.assets' do |app|
+ if app.config.respond_to?(:assets)
+ app.config.assets.paths << root.join('app/javascript')
+ app.config.assets.paths << root.join('vendor/javascript')
+ app.config.assets.precompile += %w[spree_multi_vendor_manifest]
+ end
end
def self.activate
@@ -25,10 +27,6 @@ def self.activate
end
end
- def self.api_v1_available?
- @@api_v1_available ||= Gem::Specification.find_all_by_name('spree_api_v1').any?
- end
-
- config.to_prepare &method(:activate).to_proc
+ config.to_prepare(&method(:activate).to_proc)
end
end
diff --git a/lib/spree_multi_vendor/factories.rb b/lib/spree_multi_vendor/factories.rb
index 2934112c..a286d57e 100644
--- a/lib/spree_multi_vendor/factories.rb
+++ b/lib/spree_multi_vendor/factories.rb
@@ -1,7 +1,4 @@
-FactoryBot.define do
- GEM_ROOT = File.dirname(File.dirname(File.dirname(__FILE__)))
+require 'factory_bot'
+require 'factory_bot_rails'
- Dir[File.join(GEM_ROOT, 'spec', 'factories', '**', '*.rb')].each do |factory|
- require(factory)
- end
-end
+Dir[File.join(__dir__, 'factories', '**', '*.rb')].each { |file| require file }
diff --git a/lib/spree_multi_vendor/factories/billing_address_factory.rb b/lib/spree_multi_vendor/factories/billing_address_factory.rb
new file mode 100644
index 00000000..f70181fd
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/billing_address_factory.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :billing_address, class: Spree::BillingAddress, parent: :address do
+ trait :skip_mainstreet_validation do
+ skip_mainstreet_validation { true }
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/country_factory.rb b/lib/spree_multi_vendor/factories/country_factory.rb
new file mode 100644
index 00000000..9f204cd6
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/country_factory.rb
@@ -0,0 +1,43 @@
+FactoryBot.define do
+ factory :canada_country, class: Spree::Country do
+ iso { 'CA' }
+ iso3 { 'CAN' }
+ name { 'Canada' }
+ iso_name { 'CANADA' }
+ zipcode_required { true }
+ end
+
+ factory :usa_country, class: Spree::Country do
+ iso { 'US' }
+ iso3 { 'USA' }
+ name { 'United States' }
+ iso_name { 'UNITED STATES' }
+ states_required { true }
+ zipcode_required { true }
+ end
+
+ factory :poland_country, class: Spree::Country do
+ iso { 'PL' }
+ iso3 { 'POL' }
+ name { 'Poland' }
+ iso_name { 'POLAND' }
+ zipcode_required { true }
+ end
+
+ factory :uk_country, class: Spree::Country do
+ iso { 'GB' }
+ iso3 { 'GBR' }
+ name { 'United Kingdom' }
+ iso_name { 'UNITED KINGDOM' }
+ zipcode_required { true }
+ end
+
+ factory :ireland_country, class: Spree::Country do
+ iso { 'IE' }
+ iso3 { 'IRL' }
+ name { 'Ireland' }
+ iso_name { 'UNITED KINGDOM' }
+ zipcode_required { false }
+ states_required { true }
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/marketplace_commission_factory.rb b/lib/spree_multi_vendor/factories/marketplace_commission_factory.rb
new file mode 100644
index 00000000..b8c66a55
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/marketplace_commission_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :marketplace_commission, class: Spree::MarketplaceCommission do
+ amount { 10.0 }
+ active { true }
+ label { Spree::MarketplaceCommission::ITEM_LABEL }
+ order { build(:order) }
+ feeable { build(:line_item) }
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/order_factory.rb b/lib/spree_multi_vendor/factories/order_factory.rb
new file mode 100644
index 00000000..ba067da2
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/order_factory.rb
@@ -0,0 +1,9 @@
+if FactoryBot.factories.registered?(:order)
+ FactoryBot.modify do
+ factory :order, class: Spree::Order do
+ trait :with_external_id do
+ external_id { SecureRandom.uuid }
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/refund_factory.rb b/lib/spree_multi_vendor/factories/refund_factory.rb
new file mode 100644
index 00000000..a24cbd61
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/refund_factory.rb
@@ -0,0 +1,7 @@
+if FactoryBot.factories.registered?(:refund)
+ FactoryBot.modify do
+ factory :refund, class: Spree::Refund do
+ association(:refunder, factory: :admin_user)
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/returns_address_factory.rb b/lib/spree_multi_vendor/factories/returns_address_factory.rb
new file mode 100644
index 00000000..396e5629
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/returns_address_factory.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :returns_address, class: Spree::ReturnsAddress, parent: :address do
+ trait :skip_mainstreet_validation do
+ skip_mainstreet_validation { true }
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_customer_return_factory.rb b/lib/spree_multi_vendor/factories/vendor_customer_return_factory.rb
new file mode 100644
index 00000000..d5bea128
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_customer_return_factory.rb
@@ -0,0 +1,37 @@
+FactoryBot.define do
+ # for the case when you want to supply existing return items instead of generating some
+ factory :vendor_customer_return_without_return_items, class: Spree::CustomerReturn do
+ association(:stock_location, factory: :stock_location)
+
+ before(:create) do |customer_return, _evaluator|
+ if customer_return.store.nil?
+ default_store = Spree::Store.default.persisted? ? Spree::Store.default : nil
+ store = default_store || create(:store)
+
+ customer_return.store = store
+ end
+ end
+
+ factory :vendor_customer_return, class: Spree::CustomerReturn do
+ transient do
+ line_items_count { 1 }
+ return_items_count { line_items_count }
+ end
+
+ before(:create) do |customer_return, evaluator|
+ shipped_order = create(:vendor_shipped_order, line_items_count: evaluator.line_items_count, store: customer_return.store, vendor: customer_return.stock_location.vendor)
+
+ shipped_order.inventory_units.take(evaluator.return_items_count).each do |inventory_unit|
+ return_authorization = create(:return_authorization, order: shipped_order, stock_location: customer_return.stock_location)
+ customer_return.return_items << build(:return_item, inventory_unit: inventory_unit, return_authorization: return_authorization)
+ end
+ end
+
+ factory :vendor_customer_return_with_accepted_items do
+ after(:create) do |customer_return|
+ customer_return.return_items.each(&:accept!)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_factory.rb b/lib/spree_multi_vendor/factories/vendor_factory.rb
new file mode 100644
index 00000000..03d542f5
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_factory.rb
@@ -0,0 +1,39 @@
+FactoryBot.define do
+ factory :vendor, class: Spree::Vendor do
+ name { FFaker::Company.name }
+ contact_person_email { FFaker::Internet.email }
+ invited_by_email { false }
+ state { :approved }
+
+ factory :approved_vendor do
+ state { :approved }
+ end
+
+ factory :onboarding_vendor do
+ state { :onboarding }
+ end
+
+ factory :rejected_vendor do
+ state { :rejected }
+ end
+
+ factory :invited_vendor do
+ invited_by_email { true }
+ state { :invited }
+ end
+
+ trait :with_logo do
+ before(:create) do |vendor|
+ file = File.new(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg')
+ vendor.logo.attach(io: file, filename: 'logo', content_type: 'image/jpeg')
+ end
+ end
+
+ trait :with_cover_photo do
+ before(:create) do |vendor|
+ file = File.new(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg')
+ vendor.cover_photo.attach(io: file, filename: 'logo', content_type: 'image/jpeg')
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_line_item_factory.rb b/lib/spree_multi_vendor/factories/vendor_line_item_factory.rb
new file mode 100644
index 00000000..6d46daec
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_line_item_factory.rb
@@ -0,0 +1,16 @@
+FactoryBot.define do
+ factory :vendor_line_item, class: Spree::LineItem, parent: :line_item do
+ product do
+ price_currency = order&.currency || Spree::Store.default.default_currency
+ product_vendor = vendor || order.vendor || create(:approved_vendor)
+
+ product = if order&.store&.present?
+ create(:vendor_product_in_stock, stores: [order.store], vendor: product_vendor, price_currency: price_currency)
+ else
+ create(:vendor_product_in_stock, vendor: product_vendor, price_currency: price_currency)
+ end
+
+ product
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_order_factory.rb b/lib/spree_multi_vendor/factories/vendor_order_factory.rb
new file mode 100644
index 00000000..d6515665
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_order_factory.rb
@@ -0,0 +1,202 @@
+FactoryBot.define do
+ factory :splitted_order, parent: :completed_order_with_totals do
+ state { 'splitted' }
+
+ transient do
+ payment_gateway { create(:credit_card_payment_method) }
+ end
+
+ after(:create) do |order, evaluator|
+ create(:payment,
+ order: order,
+ amount: order.total,
+ payment_method: evaluator.payment_gateway,
+ )
+ end
+ end
+
+ factory :placed_order, parent: :order_with_line_items do
+ state { 'confirm' }
+ completed_at { Time.current }
+
+ after(:create) do |order, evaluator|
+ create(
+ :payment,
+ amount: order.total,
+ order: order,
+ payment_method: evaluator.payment_gateway
+ )
+ end
+ end
+
+ factory :placed_order_with_vendor_items, parent: :order do
+ state { 'confirm' }
+ completed_at { Time.current }
+
+ bill_address
+ ship_address
+
+ transient do
+ line_items_count { 1 }
+ shipment_cost { 100 }
+ vendor { create(:approved_vendor) }
+ payment_gateway { create(:credit_card_payment_method) }
+ end
+
+ after(:create) do |order, evaluator|
+ create_list(
+ :vendor_line_item,
+ evaluator.line_items_count,
+ order: order,
+ price: evaluator.line_items_price,
+ vendor: evaluator.vendor,
+ platform_fee_total: evaluator.line_items_price * 0.2
+ )
+
+ order.line_items.reload
+
+ create(:vendor_shipment, order: order, cost: evaluator.shipment_cost, vendor: evaluator.vendor)
+ order.shipments.reload
+
+ order.update_with_updater!
+
+ create(
+ :payment,
+ amount: order.total,
+ order: order,
+ payment_method: evaluator.payment_gateway
+ )
+ end
+ end
+
+ factory :vendor_order, parent: :order do
+ parent { create(:order) }
+ vendor { create(:approved_vendor) }
+ end
+
+ factory :vendor_order_with_totals, class: Spree::Order, parent: :order do
+ parent { create(:order) }
+
+ transient do
+ source { nil }
+ end
+
+ after(:create) do |order, evaluator|
+ create(:vendor_line_item,
+ order: order,
+ price: evaluator.line_items_price,
+ vendor: order.vendor)
+
+ order.line_items.reload # to ensure order.line_items is accessible after
+ end
+ end
+
+ factory :vendor_order_with_line_items, class: Spree::Order, parent: :order do
+ bill_address
+ ship_address
+ vendor { create(:approved_vendor) }
+ parent { create(:order) }
+
+ transient do
+ line_items_count { 1 }
+ without_line_items { false }
+ shipment_cost { 100 }
+ shipping_method_filter { Spree::ShippingMethod::DISPLAY_ON_FRONT_END }
+ end
+
+ after(:create) do |order, evaluator|
+ unless evaluator.without_line_items
+ create_list(:vendor_line_item, evaluator.line_items_count,
+ order: order,
+ price: evaluator.line_items_price,
+ vendor: order.vendor,
+ platform_fee_total: evaluator.line_items_price * 0.2)
+
+ order.line_items.reload
+ end
+
+ create(:vendor_shipment, order: order, cost: evaluator.shipment_cost, vendor: order.vendor)
+ order.shipments.reload
+
+ order.update_with_updater!
+ end
+
+ factory :vendor_completed_order_with_totals do
+ state { 'complete' }
+
+ after(:create) do |order, evaluator|
+ order.refresh_shipment_rates(evaluator.shipping_method_filter)
+ order.update_column(:completed_at, Time.current)
+ end
+
+ factory :vendor_completed_order_with_pending_payment do
+ after(:create) do |order|
+ payment_method = if order.parent.present? && order.parent.payments.first&.payment_method
+ order.parent.payments.first.payment_method
+ else
+ SpreeStripe::Gateway.first || create(:credit_card_payment_method, stores: [order.store])
+ end
+
+ create(:payment,
+ amount: order.total,
+ order: order,
+ payment_method: payment_method)
+ end
+ end
+
+ factory :vendor_completed_order_with_store_credit_payment do
+ after(:create) do |order|
+ store_credit = create(:store_credit, amount: order.total, store: order.store, user: order.user)
+ payment_method = create(:store_credit_payment_method, stores: [order.store])
+
+ create(:store_credit_payment, amount: order.total, order: order, source: store_credit, payment_method: payment_method)
+ end
+ end
+
+ factory :vendor_order_ready_to_ship do
+ payment_state { 'paid' }
+ shipment_state { 'ready' }
+
+ transient do
+ payment_gateway { nil }
+ with_payment { true }
+ end
+
+ after(:create) do |order, evaluator|
+ payment_method = if order.parent.present? && order.parent.payments.first&.payment_method
+ order.parent.payments.first.payment_method
+ else
+ evaluator.payment_gateway || order.store.payment_methods.where.not(type: 'Spree::PaymentMethod::StoreCredit').first || create(:credit_card_payment_method, stores: [order.store])
+ end
+
+ if evaluator.with_payment
+ create(
+ :payment,
+ amount: order.total,
+ order: order,
+ state: 'completed',
+ payment_method: payment_method
+ )
+ end
+
+ order.shipments.each do |shipment|
+ shipment.inventory_units.update_all state: 'on_hand'
+ shipment.update_column('state', 'ready')
+ end
+ order.reload
+ end
+
+ factory :vendor_shipped_order do
+ after(:create) do |order|
+ order.shipments.each do |shipment|
+ shipment.inventory_units.update_all state: 'shipped'
+ shipment.update_column('state', 'shipped')
+ end
+ order.update_column('shipment_state', 'shipped')
+ order.reload
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_product_factory.rb b/lib/spree_multi_vendor/factories/vendor_product_factory.rb
new file mode 100644
index 00000000..808fae60
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_product_factory.rb
@@ -0,0 +1,51 @@
+FactoryBot.define do
+ factory :vendor_product, class: Spree::Product do
+ sequence(:name) { |n| "Product #{n}#{Kernel.rand(9999)}" }
+ description { generate(:random_description) }
+ price { 19.99 }
+ cost_price { 17.00 }
+ sku { generate(:sku) }
+ available_on { 1.year.ago }
+ make_active_at { 1.year.ago }
+ deleted_at { nil }
+ shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) }
+ status { 'active' }
+ tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) }
+ vendor { create(:vendor) }
+
+ transient do
+ price_currency { Spree::Store.default.default_currency }
+ end
+
+ before(:create) do |product|
+ if product.stores.empty?
+ default_store = Spree::Store.default.persisted? ? Spree::Store.default : nil
+ store = default_store || create(:store)
+
+ product.stores << [store]
+ end
+ end
+
+ after(:create) do |product, evaluator|
+ product.vendor.stock_locations.each do |stock_location|
+ stock_location.propagate_variant(product.master) unless stock_location.stock_items.exists?(variant: product.master)
+ end
+
+ product_price = product.price_in(evaluator.price_currency)
+ product_price.amount = evaluator.price
+ product_price.save!
+ end
+
+ factory :vendor_product_in_stock do
+ after :create do |product|
+ product.master.stock_items.first.adjust_count_on_hand(10)
+ end
+
+ trait :without_backorder do
+ after :create do |product|
+ product.master.stock_items.update_all(backorderable: false)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_rule_factory.rb b/lib/spree_multi_vendor/factories/vendor_rule_factory.rb
new file mode 100644
index 00000000..35182cf2
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_rule_factory.rb
@@ -0,0 +1,19 @@
+FactoryBot.define do
+ factory :vendor_taxon_rule, class: SpreeMultiVendor::TaxonRules::Vendor do
+ trait :is_equal_to do
+ match_policy { :is_equal_to }
+ end
+
+ trait :is_not_equal_to do
+ match_policy { :is_not_equal_to }
+ end
+
+ trait :contains do
+ match_policy { :contains }
+ end
+
+ trait :does_not_contain do
+ match_policy { :does_not_contain }
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_shipment_factory.rb b/lib/spree_multi_vendor/factories/vendor_shipment_factory.rb
new file mode 100644
index 00000000..76d718b6
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_shipment_factory.rb
@@ -0,0 +1,36 @@
+FactoryBot.define do
+ factory :vendor_shipment, class: Spree::Shipment do
+ tracking { 'U10000' }
+ cost { 100.00 }
+ state { 'pending' }
+ order
+ stock_location { vendor.stock_locations.first }
+ vendor
+
+ transient do
+ shipping_method_attributes { {} }
+ map_inventory_units { true }
+ end
+
+ after(:create) do |shipment, evaluator|
+ shipping_method_name = evaluator.shipping_method_attributes[:name]
+ shipping_method_scope = { name: shipping_method_name } if shipping_method_name.present?
+
+ shipping_method = shipment.vendor.shipping_methods.where(shipping_method_scope).first ||
+ create(:shipping_method, vendor: shipment.vendor, **evaluator.shipping_method_attributes)
+
+ shipment.add_shipping_method(shipping_method, true)
+
+ if evaluator.map_inventory_units
+ shipment.order.line_items.map do |line_item|
+ shipment.inventory_units.create(
+ order_id: shipment.order_id,
+ variant_id: line_item.variant_id,
+ line_item_id: line_item.id,
+ quantity: line_item.quantity
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_user_factory.rb b/lib/spree_multi_vendor/factories/vendor_user_factory.rb
new file mode 100644
index 00000000..9562f2af
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_user_factory.rb
@@ -0,0 +1,13 @@
+FactoryBot.define do
+ factory :vendor_user, class: Spree.admin_user_class, parent: :admin_user do
+ without_admin_role
+
+ transient do
+ vendor { nil }
+ end
+
+ after(:create) do |user, evaluator|
+ evaluator.vendor.add_user(user) if evaluator.vendor.present?
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/factories/vendor_variant_factory.rb b/lib/spree_multi_vendor/factories/vendor_variant_factory.rb
new file mode 100644
index 00000000..392c5345
--- /dev/null
+++ b/lib/spree_multi_vendor/factories/vendor_variant_factory.rb
@@ -0,0 +1,32 @@
+FactoryBot.define do
+ factory :vendor_variant, class: Spree::Variant do
+ price { 19.99 }
+ cost_price { 17.00 }
+ sku { generate(:sku) }
+ weight { generate(:random_float) }
+ height { generate(:random_float) }
+ width { generate(:random_float) }
+ depth { generate(:random_float) }
+ is_master { 0 }
+ track_inventory { true }
+
+ product { |p| p.association(:vendor_product, stores: [create(:store)]) }
+ option_values { [create(:option_value)] }
+
+ after(:create) do |variant|
+ variant.product.vendor.stock_locations.each do |stock_location|
+ stock_location.propagate_variant(variant)
+ end
+ end
+
+ factory :vendor_variant_in_stock do
+ transient do
+ stock_count { 10 }
+ end
+
+ after(:create) do |variant, evaluator|
+ variant.stock_items.first.adjust_count_on_hand(evaluator.stock_count)
+ end
+ end
+ end
+end
diff --git a/lib/spree_multi_vendor/testing_support/authorization_helpers.rb b/lib/spree_multi_vendor/testing_support/authorization_helpers.rb
new file mode 100644
index 00000000..864b29dc
--- /dev/null
+++ b/lib/spree_multi_vendor/testing_support/authorization_helpers.rb
@@ -0,0 +1,24 @@
+module SpreeMultiVendor
+ module AuthorizationHelpers
+ module Controller
+ def stub_vendor_authorization!
+ stub_authorization!
+ let(:admin_user) { vendor.users.first || create(:vendor_user, vendor: vendor) }
+ end
+ end
+
+ module Request
+ def stub_vendor_authorization!
+ stub_authorization!
+ let(:admin_user) { vendor.users.first || create(:vendor_user, vendor: vendor) }
+ end
+ end
+ end
+end
+
+RSpec.configure do |config|
+ config.extend SpreeMultiVendor::AuthorizationHelpers::Controller, type: :controller
+ config.extend SpreeMultiVendor::AuthorizationHelpers::Request, type: :feature
+ config.extend SpreeMultiVendor::AuthorizationHelpers::Request, type: :request
+ config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :request
+end
diff --git a/lib/spree_multi_vendor/version.rb b/lib/spree_multi_vendor/version.rb
index 22b591dc..d66b45f6 100644
--- a/lib/spree_multi_vendor/version.rb
+++ b/lib/spree_multi_vendor/version.rb
@@ -1,11 +1,7 @@
module SpreeMultiVendor
- VERSION = '2.5.0'
+ VERSION = '3.0.0.alpha'.freeze
- module_function
-
- # Returns the version of the currently loaded SpreeMultiVendor as a
- #
Gem::Version .
- def version
- Gem::Version.new VERSION
+ def gem_version
+ Gem::Version.new(VERSION)
end
end
diff --git a/lib/tasks/vendors.rake b/lib/tasks/vendors.rake
deleted file mode 100644
index 472a3cd5..00000000
--- a/lib/tasks/vendors.rake
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace :spree_multi_vendor do
- namespace :sample do
- desc 'Create sample Vendor and Vendor User'
- task create: :environment do
- require File.join(File.dirname(__FILE__), '..', '..', 'db', 'default', 'vendors.rb')
- end
- end
-end
\ No newline at end of file
diff --git a/lib/testing_support/authorization_helpers.rb b/lib/testing_support/authorization_helpers.rb
new file mode 100644
index 00000000..864b29dc
--- /dev/null
+++ b/lib/testing_support/authorization_helpers.rb
@@ -0,0 +1,24 @@
+module SpreeMultiVendor
+ module AuthorizationHelpers
+ module Controller
+ def stub_vendor_authorization!
+ stub_authorization!
+ let(:admin_user) { vendor.users.first || create(:vendor_user, vendor: vendor) }
+ end
+ end
+
+ module Request
+ def stub_vendor_authorization!
+ stub_authorization!
+ let(:admin_user) { vendor.users.first || create(:vendor_user, vendor: vendor) }
+ end
+ end
+ end
+end
+
+RSpec.configure do |config|
+ config.extend SpreeMultiVendor::AuthorizationHelpers::Controller, type: :controller
+ config.extend SpreeMultiVendor::AuthorizationHelpers::Request, type: :feature
+ config.extend SpreeMultiVendor::AuthorizationHelpers::Request, type: :request
+ config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :request
+end
diff --git a/spec/.rubocop.yml b/spec/.rubocop.yml
new file mode 100644
index 00000000..fb81f096
--- /dev/null
+++ b/spec/.rubocop.yml
@@ -0,0 +1,9 @@
+require: rubocop-rspec
+
+inherit_from: ../.rubocop.yml
+
+Metrics/BlockLength:
+ Enabled: false
+
+Style/BlockDelimiters:
+ Enabled: false
diff --git a/spec/controllers/spree/account/orders_controller_spec.rb b/spec/controllers/spree/account/orders_controller_spec.rb
new file mode 100644
index 00000000..869e7f03
--- /dev/null
+++ b/spec/controllers/spree/account/orders_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Account::OrdersController, type: :controller do
+ let(:store) { Spree::Store.default }
+
+ let!(:parent_order) { create(:splitted_order, user: user, store: store, payment_gateway: check_payment_method) }
+ let!(:order) { create(:vendor_order_ready_to_ship, parent: parent_order, user: user, store: store) }
+
+ let(:check_payment_method) { create(:check_payment_method, stores: [store]) }
+ let(:user) { create(:user) }
+
+ render_views
+
+ before do
+ allow(controller).to receive(:current_store).and_return(store)
+ allow(controller).to receive(:spree_login_path).and_return('/login')
+ end
+
+ describe '#show' do
+ subject { get :show, params: { id: parent_order.number } }
+
+ context 'when user is logged in' do
+ before do
+ allow(controller).to receive(:try_spree_current_user).and_return(user)
+ allow(controller).to receive(:spree_current_user).and_return(user)
+ end
+
+ it 'shows the order details' do
+ subject
+ expect(response).to have_http_status(:ok)
+
+ expect(assigns(:order)).to eq(parent_order)
+
+ expect(assigns(:shipments).length).to eq(2)
+ expect(assigns(:shipments)).to contain_exactly(order.shipments.first, parent_order.shipments.first)
+ end
+
+ it 'renders the order details' do
+ subject
+ expect(response).to render_template(:show)
+ end
+
+ context 'when order number does not exist' do
+ subject { get :show, params: { id: 'invalid' } }
+
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
+ context 'when user is not logged in' do
+ it 'redirects to login page' do
+ expect(subject).to have_http_status(302)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/base_controller_spec.rb b/spec/controllers/spree/admin/base_controller_spec.rb
new file mode 100644
index 00000000..138606ca
--- /dev/null
+++ b/spec/controllers/spree/admin/base_controller_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Spree::Admin::BaseController, type: :controller do
+ let(:store) { Spree::Store.default }
+
+ describe '#current_vendor' do
+ let!(:vendor) { create(:approved_vendor) }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+ let(:store) { Spree::Store.default }
+
+ subject { controller.current_vendor }
+
+ context 'spree controllers with current store' do
+ before do
+ allow(controller).to receive(:try_spree_current_user).and_return(admin_user)
+ allow(controller).to receive(:current_store).and_return(store)
+ end
+
+ context 'current_vendor_id session is set' do
+ before { session[:current_vendor_id] = vendor.id }
+
+ it { is_expected.to eq(vendor) }
+ end
+
+ context 'current_vendor_id session is not set' do
+ it { is_expected.to eq(vendor) }
+ end
+
+ context 'store admin user' do
+ before do
+ store.add_user(admin_user)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'no user' do
+ before do
+ allow(controller).to receive(:current_store).and_return(store)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/dashboard_controller_spec.rb b/spec/controllers/spree/admin/dashboard_controller_spec.rb
new file mode 100644
index 00000000..5194cdc3
--- /dev/null
+++ b/spec/controllers/spree/admin/dashboard_controller_spec.rb
@@ -0,0 +1,238 @@
+require 'spec_helper'
+
+describe Spree::Admin::DashboardController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:store) { Spree::Store.default }
+
+ describe '#show' do
+ it 'renders welcome page' do
+ get :show
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe '#analytics' do
+ context 'view' do
+ it 'renders analytics' do
+ get :analytics
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'with vendor' do
+ context 'missing vendor' do
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ before do
+ get :analytics, params: { vendor_id: 'missing' }
+ end
+
+ it 'fallbacks to default vendor for this user' do
+ expect(controller.current_vendor).to eq(vendor)
+ end
+ end
+
+ context 'vendor from different account' do
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:other_vendor) { create(:approved_vendor) }
+ let!(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ before { get(:analytics, params: { vendor_id: other_vendor.id }) }
+
+ it 'fallbacks to default vendor for this user' do
+ expect(controller.current_vendor).to eq(vendor)
+ end
+ end
+ end
+
+ xcontext 'products_scope' do
+ let!(:product1) { create(:product_in_stock) }
+ let!(:product2) { create(:product_in_stock) }
+ let!(:other_product) { create(:product, stores: [create(:store)]) }
+
+ subject do
+ @controller.instance_variable_get(:@products_scope)
+ end
+
+ context 'without vendor' do
+ it 'is set to all products in the store' do
+ get :analytics
+ expect(subject).to contain_exactly(product1, product2)
+ end
+ end
+
+ xcontext 'with vendors' do
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:other_vendor) { create(:approved_vendor) }
+ let!(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ before do
+ product1.update_column(:vendor_id, vendor.id)
+ product2.update_column(:vendor_id, other_vendor.id)
+ end
+
+ it 'is set to products from the current store and vendor' do
+ get :analytics, params: { vendor_id: vendor.id }
+ expect(subject).to contain_exactly(product1)
+ end
+ end
+ end
+
+ xcontext 'orders_scope' do
+ let!(:order1) { create(:completed_order_with_totals) }
+ let!(:order2) { create(:completed_order_with_totals) }
+ let!(:incomplete_order) { create(:order) }
+ let!(:other_store) { create(:store) }
+ let!(:order_in_other_store) { create(:completed_order_with_totals, store: other_store) }
+
+ subject do
+ @controller.instance_variable_get(:@orders_scope)
+ end
+
+ context 'currency' do
+ let!(:order_in_other_currency) { create(:completed_order_with_totals, currency: 'EUR') }
+
+ context 'with default currency' do
+ it 'is set to all orders with the default currency' do
+ get :analytics
+ expect(subject.unscope(:group)).to contain_exactly(order1, order2)
+ end
+ end
+
+ context 'with currency=EUR' do
+ it 'is set to all orders with the given currency' do
+ get :analytics, params: { analytics_currency: 'EUR' }
+ expect(subject.unscope(:group)).to contain_exactly(order_in_other_currency)
+ end
+ end
+ end
+
+ xcontext 'with vendors' do
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:other_vendor) { create(:approved_vendor) }
+ let!(:admin_user) { create(:vendor_user, vendor: vendor) }
+ let!(:order1) { create(:vendor_completed_order_with_totals, vendor: vendor) }
+ let!(:order2) { create(:vendor_completed_order_with_totals, vendor: vendor) }
+
+ it 'is set to orders from the current store and vendor' do
+ get :analytics, params: { vendor_id: vendor.id }
+ expect(subject.unscope(:group)).to contain_exactly(order1)
+ end
+ end
+
+ context 'grouping by time range' do
+ let!(:order_from_this_week) { create(:completed_order_with_totals) }
+ let!(:order_from_this_month) { create(:completed_order_with_totals) }
+
+ before do
+ order_from_this_week.update_column(:completed_at, 5.days.ago)
+ order_from_this_month.update_column(:completed_at, 25.days.ago)
+ end
+
+ context 'default time range (last 24 hours)' do
+ it 'is set to the orders from the current store completed in the last 24 hours' do
+ get :analytics
+
+ num_orders_by_hour = subject.count.values
+ expect(num_orders_by_hour.sum).to eq(2)
+ end
+ end
+ end
+ end
+
+ xcontext 'product_units_sold' do
+ let!(:incomplete_order) { create(:order) }
+ let!(:order_in_other_store) { create(:completed_order_with_totals, store: create(:store)) }
+ let!(:order1) { create(:completed_order_with_totals) }
+ let!(:order2) { create(:completed_order_with_totals) }
+ let(:product1) { order1.line_items.first.product }
+ let(:product2) { order2.line_items.first.product }
+
+ subject do
+ @controller.instance_variable_get(:@product_units_sold)
+ end
+
+ context 'currency' do
+ let!(:order_in_other_currency) { create(:completed_order_with_totals, currency: 'EUR') }
+
+ context 'with default currency' do
+ it 'is set to all orders with the default currency' do
+ get :analytics
+ expect(subject.to_a).to contain_exactly([product1.name, 1], [product2.name, 1])
+ end
+ end
+
+ context 'with currency=EUR' do
+ it 'is set to all orders with the given currency' do
+ get :analytics, params: { analytics_currency: 'EUR' }
+ expect(subject.to_a).to contain_exactly([order_in_other_currency.line_items.take.product.name, 1])
+ end
+ end
+ end
+
+ xcontext 'with vendors' do
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:other_vendor) { create(:approved_vendor) }
+ let!(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ before do
+ order1.update_column(:vendor_id, vendor.id)
+ order2.update_column(:vendor_id, other_vendor.id)
+ end
+
+ it 'is counts units sold for orders the current store and vendor' do
+ get :analytics, params: { vendor_id: vendor.id }
+ expect(subject.to_a).to contain_exactly([product1.name, 1])
+ end
+ end
+
+ context 'with orders outside of time range' do
+ let!(:order_from_this_week) { create(:completed_order_with_totals) }
+ let!(:order_from_this_month) { create(:completed_order_with_totals) }
+ let!(:order_from_this_week_product) { order_from_this_week.line_items.first.product }
+ let!(:order_from_this_month_product) { order_from_this_month.line_items.first.product }
+
+ before do
+ order_from_this_week.update_column(:completed_at, 5.days.ago)
+ order_from_this_month.update_column(:completed_at, 25.days.ago)
+ end
+
+ it 'returns items sold in last day by default' do
+ get :analytics
+ expect(subject.to_a).to contain_exactly([product1.name, 1], [product2.name, 1])
+ end
+
+ it 'items sold in last 7 days' do
+ get :analytics, params: { analytics_time_range: 7 }
+ expect(subject.to_a).to contain_exactly([product1.name, 1], [product2.name, 1], [order_from_this_week_product.name, 1])
+ end
+
+ it 'items sold in last 30 days' do
+ get :analytics, params: { analytics_time_range: 30 }
+ expect(subject.to_a).to contain_exactly([product1.name, 1], [product2.name, 1],
+ [order_from_this_week_product.name, 1], [order_from_this_month_product.name, 1])
+ end
+ end
+
+ context 'with more line items and quantity' do
+ let!(:order_from_this_week) { create(:completed_order_with_totals) }
+ let!(:order_from_this_week_product) { order_from_this_week.line_items.first.product }
+ let!(:order1_line_item) { create(:line_item, product: product1, order: order1, quantity: 4) }
+
+ before do
+ order_from_this_week.update_column(:completed_at, 5.days.ago)
+ order_from_this_week.line_items.first.update_column(:quantity, 3)
+ order2.line_items.first.update_column(:quantity, 10)
+ end
+
+ it 'returns items sold by summing quantities' do
+ get :analytics, params: { analytics_time_range: 7 }
+ expect(subject.to_a).to contain_exactly([product1.name, 5], [product2.name, 10], [order_from_this_week_product.name, 3])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/exports_controller_spec.rb b/spec/controllers/spree/admin/exports_controller_spec.rb
new file mode 100644
index 00000000..156771ff
--- /dev/null
+++ b/spec/controllers/spree/admin/exports_controller_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Admin::ExportsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:vendor) { create(:approved_vendor) }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+ let(:store) { Spree::Store.default }
+
+ before do
+ allow(controller).to receive(:current_vendor).and_return(vendor)
+ allow(controller).to receive(:try_spree_current_user).and_return(admin_user)
+ end
+
+ describe '#new' do
+ it 'renders the new template' do
+ get :new, params: { export: { type: 'Spree::Exports::Orders' } }
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:new)
+ expect(assigns(:object)).to be_a_new(Spree::Export)
+ end
+ end
+
+ describe '#create' do
+ let(:export_params) do
+ {
+ export: {
+ type: 'Spree::Exports::Orders',
+ search_params: { 'created_at_gt' => 1.week.ago.to_s }.to_json
+ }
+ }
+ end
+
+ it 'creates a new export with vendor_id set' do
+ expect {
+ post :create, params: export_params
+ }.to change(Spree::Export, :count).by(1)
+
+ export = Spree::Export.last
+ expect(export.vendor).to eq(vendor)
+ expect(export.user).to eq(admin_user)
+ expect(export.type).to eq('Spree::Exports::Orders')
+ expect(export.search_params).to be_present
+ expect(response).to redirect_to(spree.admin_exports_path)
+ end
+
+ it 'sets flash message after successful creation' do
+ post :create, params: export_params
+ expect(flash[:success]).to eq(Spree.t('admin.export_created'))
+ end
+ end
+
+ describe '#index' do
+ let(:other_vendor) { create(:approved_vendor) }
+ let!(:vendor_export) { create(:product_export, vendor: vendor, user: admin_user, store: store) }
+ let!(:other_vendor_export) { create(:product_export, vendor: other_vendor, store: store) }
+ let!(:admin_export) { create(:product_export, vendor: nil, store: store) }
+
+ it 'lists only exports belonging to current vendor' do
+ get :index
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:index)
+
+ expect(assigns(:collection)).to include(vendor_export)
+ expect(assigns(:collection)).not_to include(other_vendor_export)
+ expect(assigns(:collection)).not_to include(admin_export)
+ end
+ end
+
+ describe '#show' do
+ let(:export) { create(:product_export, vendor: vendor, user: admin_user, store: store) }
+
+ before do
+ allow_any_instance_of(Spree::Exports::Products).to receive_message_chain(:attachment, :url).and_return('http://example.com/test.csv')
+ end
+
+ subject { get :show, params: { id: export.id } }
+
+ it 'downloads the export' do
+ subject
+ expect(response).to have_http_status(:see_other)
+ expect(response.headers['Location']).to eq(export.attachment.url)
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/imports_controller_spec.rb b/spec/controllers/spree/admin/imports_controller_spec.rb
new file mode 100644
index 00000000..e477ab17
--- /dev/null
+++ b/spec/controllers/spree/admin/imports_controller_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Admin::ImportsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:vendor) { create(:approved_vendor) }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+ let(:store) { Spree::Store.default }
+
+ let(:attachment) { Rack::Test::UploadedFile.new(File.join(Spree::Core::Engine.root, 'spec/fixtures/files', 'products_import.csv'), 'text/csv') }
+ let(:csv_content) { File.read(File.join(Spree::Core::Engine.root, 'spec/fixtures/files', 'products_import.csv')) }
+
+ before do
+ allow(controller).to receive(:current_vendor).and_return(vendor)
+ allow(controller).to receive(:try_spree_current_user).and_return(admin_user)
+ end
+
+ describe 'GET #new' do
+ it 'renders the new template' do
+ get :new, params: { import: { type: 'Spree::Imports::Products' } }
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:new)
+ expect(assigns(:object)).to be_a_new(Spree::Import)
+ end
+ end
+
+ describe 'POST #create' do
+ let(:import_params) { { type: Spree::Import.available_types.first.to_s, attachment: attachment } }
+
+ it 'creates a new import and redirects to show' do
+ expect {
+ post :create, params: { import: import_params }
+ }.to change(Spree::Import, :count).by(1)
+
+ import = Spree::Import.last
+ expect(response).to redirect_to(spree.admin_import_path(import))
+ expect(import.user).to eq(admin_user)
+ expect(import.owner).to eq(vendor)
+ expect(import.type).to eq(Spree::Import.available_types.first.to_s)
+ expect(import.attachment).to be_attached
+ expect(import.attachment.filename.to_s).to eq('products_import.csv')
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/orders_controller_spec.rb b/spec/controllers/spree/admin/orders_controller_spec.rb
new file mode 100644
index 00000000..4efb27b0
--- /dev/null
+++ b/spec/controllers/spree/admin/orders_controller_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Spree::Admin::OrdersController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:admin_user) { create(:admin_user) }
+ let(:store) { Spree::Store.default }
+
+ before do
+ allow(controller).to receive(:current_ability).and_call_original
+ end
+
+ describe 'GET #index' do
+ let(:vendor) { create(:vendor, name: 'Test Vendor') }
+
+ let!(:main_order) { create(:completed_order_with_totals, store: store, vendor: nil) }
+
+ let!(:parent_order_1) { create(:completed_order_with_totals, store: store, vendor: nil) }
+ let!(:vendor_order_1) { create(:vendor_completed_order_with_totals, store: store, vendor: vendor, parent: parent_order_1) }
+
+ let!(:parent_order_2) { create(:completed_order_with_totals, store: store, vendor: nil) }
+ let!(:vendor_order_2) { create(:vendor_completed_order_with_totals, store: store, parent: parent_order_2) }
+
+ context 'when vendor is present' do
+ it 'filters orders by vendor ID' do
+ get :index, params: { vendor_id: vendor.id }
+
+ expect(assigns(:orders)).to contain_exactly(parent_order_1)
+ end
+
+ it 'filters orders by vendor slug' do
+ get :index, params: { vendor_id: vendor.slug }
+
+ expect(assigns(:orders)).to contain_exactly(parent_order_1)
+ end
+ end
+
+ context 'when vendor is not present' do
+ it 'shows all orders' do
+ get :index, params: { vendor_id: nil }
+
+ expect(assigns(:orders)).to contain_exactly(main_order, parent_order_1, parent_order_2)
+ end
+ end
+
+ context 'when vendor_id is invalid' do
+ it 'redirects to the index page' do
+ get :index, params: { vendor_id: 'invalid-slug' }
+
+ expect(response).to redirect_to(spree.admin_orders_path)
+ end
+ end
+
+ context 'when current user is a vendor user' do
+ let(:vendor_user) { create(:vendor_user, vendor: vendor) }
+
+ before do
+ allow(controller).to receive(:current_spree_user).and_return(vendor_user)
+ allow(controller).to receive(:current_vendor).and_return(vendor)
+ end
+
+ it 'does not load vendor from params when current_vendor is present' do
+ get :index, params: { vendor_id: 'different-vendor-id' }
+
+ expect(assigns(:vendor)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb
new file mode 100644
index 00000000..72920e8d
--- /dev/null
+++ b/spec/controllers/spree/admin/products_controller_spec.rb
@@ -0,0 +1,169 @@
+require 'spec_helper'
+
+describe Spree::Admin::ProductsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:admin_user) { create(:admin_user) }
+
+ before do
+ allow(controller).to receive(:current_ability).and_call_original
+ end
+
+ let(:store) { Spree::Store.default }
+
+ describe 'GET #index' do
+ let(:vendor) { create(:vendor, name: 'Test Vendor') }
+ let!(:non_vendor_product) { create(:product) }
+ let!(:vendor_product) { create(:product, vendor: vendor) }
+
+ context 'when vendor is present' do
+ it 'filters products by vendor ID' do
+ get :index, params: { vendor_id: vendor.id }
+
+ expect(assigns(:collection)).to contain_exactly(vendor_product)
+ end
+
+ it 'filters products by vendor slug' do
+ get :index, params: { vendor_id: vendor.slug }
+
+ expect(assigns(:collection)).to contain_exactly(vendor_product)
+ end
+ end
+
+ context 'when vendor is not present' do
+ it 'does not filter products by vendor' do
+ get :index, params: { vendor_id: nil }
+
+ expect(assigns(:collection)).to contain_exactly(vendor_product, non_vendor_product)
+ end
+ end
+
+ context 'when vendor is not found' do
+ it 'redirects to the index page' do
+ get :index, params: { vendor_id: 'invalid-slug' }
+
+ expect(response).to redirect_to(spree.admin_products_path)
+ end
+ end
+ end
+
+ describe 'PUT #update' do
+ let!(:product) { create(:product, stores: [store], status: 'active') }
+ let(:product_params) { { status: 'draft', make_active_at: Time.current.beginning_of_day } }
+ let(:send_request) do
+ put :update, params: {
+ id: product.to_param,
+ product: product_params
+ }
+ end
+
+ context 'updating existing variants' do
+ let(:color_option_type) { Spree::OptionType.find_by(name: 'color') || create(:option_type, name: 'color', presentation: 'Color') }
+ let(:size_option_type) { Spree::OptionType.find_by(name: 'size') || create(:option_type, name: 'size', presentation: 'Size') }
+ let(:red_option_value) { color_option_type.option_values.find_by(name: 'red') || create(:option_value, name: 'red', option_type: color_option_type) }
+ let(:blue_option_value) { color_option_type.option_values.find_by(name: 'blue') || create(:option_value, name: 'blue', option_type: color_option_type) }
+ let(:small_option_value) { size_option_type.option_values.find_by(name: 'small') || create(:option_value, name: 'small', option_type: size_option_type) }
+ let(:large_option_value) { size_option_type.option_values.find_by(name: 'large') || create(:option_value, name: 'large', option_type: size_option_type) }
+
+ let(:variant1) { create(:variant, product: product, option_values: [red_option_value, small_option_value], price: 100) }
+ let(:variant2) { create(:variant, product: product, option_values: [blue_option_value, large_option_value], price: 100) }
+
+ let(:variant1_stock_item) { variant1.stock_items.first }
+ let(:variant2_stock_item) { variant2.stock_items.first }
+
+ let(:product_params) do
+ {
+ name: 'Product',
+ variants_attributes: {
+ '0' => {
+ prices_attributes: {
+ '0': { currency: 'PLN', amount: 10, id: variant1.price_in('PLN')&.id },
+ '1': { currency: 'USD', amount: 20, id: variant1.price_in('USD')&.id }
+ },
+ id: variant1.id,
+ stock_items_attributes: {
+ '0' => {
+ id: variant1_stock_item.id,
+ count_on_hand: 10,
+ stock_location_id: variant1_stock_item.stock_location_id,
+ }
+ },
+ options: [
+ {
+ id: red_option_value.id,
+ name: 'Color',
+ position: 1,
+ option_value_presentation: red_option_value.presentation,
+ option_value_name: red_option_value.name
+ },
+ {
+ id: small_option_value.id,
+ name: 'Size',
+ position: 2,
+ option_value_presentation: small_option_value.presentation,
+ option_value_name: small_option_value.name
+ }
+ ]
+ },
+ '1' => {
+ prices_attributes: {
+ '0': { currency: 'PLN', amount: 30, id: variant2.price_in('PLN')&.id },
+ '1': { currency: 'USD', amount: 40, id: variant2.price_in('USD')&.id }
+ },
+ id: variant2.id,
+ stock_items_attributes: {
+ '0' => {
+ id: variant2_stock_item.id,
+ count_on_hand: 20,
+ stock_location_id: variant2_stock_item.stock_location_id,
+ }
+ },
+ options: [
+ {
+ id: blue_option_value.id,
+ name: 'Color',
+ position: 1,
+ option_value_presentation: blue_option_value.presentation,
+ option_value_name: blue_option_value.name
+ },
+ {
+ id: large_option_value.id,
+ name: 'Size',
+ position: 2,
+ option_value_presentation: large_option_value.presentation,
+ option_value_name: large_option_value.name
+ }
+ ]
+ }
+ }
+ }
+ end
+ end
+
+ context 'with platform_fee param' do
+ let(:product_params) do
+ {
+ platform_fee: '22.4'
+ }
+ end
+
+ it 'updates spree_products.platform_fee attribute' do
+ expect { send_request }.to change { product.reload.platform_fee }.from(nil).to(22.4)
+ end
+ end
+ end
+
+ describe 'GET #bulk_modal' do
+ context 'as a vendor user' do
+ let(:vendor) { create(:approved_vendor) }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ it 'renders modal' do
+ get :bulk_modal
+
+ expect(response).to render_template('bulk_modal')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/refunds_controller_spec.rb b/spec/controllers/spree/admin/refunds_controller_spec.rb
new file mode 100644
index 00000000..5aca246a
--- /dev/null
+++ b/spec/controllers/spree/admin/refunds_controller_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Spree::Admin::RefundsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:admin_user) { create(:admin_user) }
+
+ before do
+ allow(controller).to receive(:current_ability).and_return(Spree::Dependencies.ability_class.constantize.new(admin_user))
+ end
+
+ describe '#new' do
+ let(:payment) { create(:payment, state: 'completed', order: order) }
+ subject { get :new, params: { payment_id: payment.number, order_id: order.number } }
+
+ before do
+ subject
+ end
+
+ context 'when payment order is parent order' do
+ let(:order) { create(:completed_order_with_totals, state: 'splitted') }
+
+ it 'should return error' do
+ expect(flash['error']).to eq('Authorization Failure')
+ expect(response).to be_redirect
+ end
+ end
+
+ context 'when payment order is vendor order' do
+ let(:order) { create(:vendor_completed_order_with_totals) }
+
+ it 'should not return error' do
+ expect(flash['error']).to be nil
+ expect(response).to be_successful
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/resource_controller_spec.rb b/spec/controllers/spree/admin/resource_controller_spec.rb
new file mode 100644
index 00000000..7e6b3c6b
--- /dev/null
+++ b/spec/controllers/spree/admin/resource_controller_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+module Spree
+ module Admin
+ class ExampleResourcesController < Spree::Admin::ResourceController
+ def new
+ super
+ head :ok
+ end
+
+ def model_class
+ Spree::ShippingMethod
+ end
+ end
+ end
+end
+
+RSpec.describe Spree::Admin::ExampleResourcesController, type: :controller do
+ stub_authorization!
+
+ let(:vendor) { create(:vendor) }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ after(:all) do
+ Rails.application.reload_routes!
+ end
+
+ before do
+ Spree::Core::Engine.routes.draw do
+ namespace :admin do
+ resources :example_resources
+ end
+ end
+ end
+
+ describe '#new' do
+ subject { get :new }
+
+ it 'assigns a vendor to the resource' do
+ subject
+ expect(assigns(:object).vendor).to eq(vendor)
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/shipping_methods_controller_spec.rb b/spec/controllers/spree/admin/shipping_methods_controller_spec.rb
new file mode 100644
index 00000000..556a1c7a
--- /dev/null
+++ b/spec/controllers/spree/admin/shipping_methods_controller_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Admin::ShippingMethodsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let!(:vendor) { create(:vendor) }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ describe 'GET #index' do
+ let!(:vendor_shipping_methods) { create_list(:shipping_method, 2, vendor: vendor) }
+ let!(:other_shipping_method) { create(:shipping_method) }
+
+ it 'renders vendor shipping methods' do
+ get :index
+ expect(response).to render_template(:index)
+ expect(assigns(:collection)).to contain_exactly(*vendor_shipping_methods)
+ end
+ end
+
+ describe 'POST #create' do
+ let(:shipping_method_attributes) do
+ attributes_for(
+ :shipping_method,
+ zone_ids: [zone.id],
+ shipping_category_ids: [shipping_category.id],
+ calculator_type: 'Spree::Calculator::Shipping::FlatRate'
+ )
+ end
+
+ let(:zone) { create(:zone) }
+ let(:shipping_category) { create(:shipping_category) }
+
+ it 'creates a new vendor shipping method' do
+ expect { post :create, params: { shipping_method: shipping_method_attributes } }.to change(Spree::ShippingMethod, :count).by(1)
+
+ expect(Spree::ShippingMethod.last.vendor).to eq(vendor)
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/taxons_controller_spec.rb b/spec/controllers/spree/admin/taxons_controller_spec.rb
new file mode 100644
index 00000000..f1858e88
--- /dev/null
+++ b/spec/controllers/spree/admin/taxons_controller_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Admin::TaxonsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:store) { Spree::Store.default }
+ let(:taxonomy) { create(:taxonomy, store: store) }
+
+ describe 'GET #select_options' do
+ before { Spree::Taxon.destroy_all }
+
+ let!(:automatic_taxon) { create(:taxon, taxonomy: taxonomy, name: 'Automatic Taxon', automatic: true) }
+ let!(:manual_taxon) { create(:taxon, taxonomy: taxonomy, name: 'Manual Taxon', automatic: false) }
+
+ context 'with a vendor user' do
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+ let(:vendor) { create(:approved_vendor) }
+
+ before do
+ allow(controller).to receive(:current_ability).and_return(Spree::Dependencies.ability_class.constantize.new(admin_user))
+ allow(controller).to receive(:authorize_admin).and_return(true)
+ end
+
+ it 'returns only manual taxons' do
+ get :select_options
+
+ expect(JSON.parse(response.body)).to eq([{ 'id' => manual_taxon.id, 'name' => manual_taxon.pretty_name }])
+ expect(assigns.keys).not_to include('object')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/vendor_settings_controller_spec.rb b/spec/controllers/spree/admin/vendor_settings_controller_spec.rb
new file mode 100644
index 00000000..4071bb99
--- /dev/null
+++ b/spec/controllers/spree/admin/vendor_settings_controller_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Admin::VendorSettingsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let!(:vendor) { create(:vendor, name: 'John Doe') }
+ let(:admin_user) { create(:vendor_user, vendor: vendor) }
+
+ describe 'GET #edit' do
+ it 'renders the edit template' do
+ get :edit
+ expect(response).to render_template(:edit)
+ end
+
+ context 'when there is no current vendor' do
+ before do
+ allow(controller).to receive(:current_vendor).and_return(nil)
+ end
+
+ it 'redirects to the root path' do
+ get :edit
+ expect(response).to redirect_to(spree.admin_dashboard_path)
+ end
+ end
+ end
+
+ describe 'PUT #update' do
+ subject { put :update, params: { vendor: { name: 'Jane Moe' } } }
+
+ it 'updates the vendor' do
+ subject
+
+ expect(vendor.reload.name).to eq('Jane Moe')
+ expect(response).to redirect_to(spree.edit_admin_vendor_settings_path)
+ end
+
+ context 'when there is no current vendor' do
+ before do
+ allow(controller).to receive(:current_vendor).and_return(nil)
+ end
+
+ it 'redirects to the root path' do
+ subject
+
+ expect(response).to redirect_to(spree.admin_dashboard_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/spree/admin/vendors_controller_spec.rb b/spec/controllers/spree/admin/vendors_controller_spec.rb
new file mode 100644
index 00000000..0576c9a4
--- /dev/null
+++ b/spec/controllers/spree/admin/vendors_controller_spec.rb
@@ -0,0 +1,165 @@
+require 'spec_helper'
+
+describe Spree::Admin::VendorsController, type: :controller do
+ stub_authorization!
+ render_views
+
+ let(:store) { Spree::Store.default }
+
+ describe '#index' do
+ let!(:vendor_one) { create(:approved_vendor, products_count: 1, sales_total: 10, commission_total: 100, name: 'Vendor_one') }
+ let!(:vendor_two) { create(:approved_vendor, products_count: 2, sales_total: 20, commission_total: 200) }
+
+ let(:format) { 'html' }
+
+ before do
+ get :index, format: format
+ end
+
+ it 'renders index' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'return vendors' do
+ expect(assigns(:collection)).to include(vendor_one)
+ expect(assigns(:collection)).to include(vendor_two)
+ end
+
+ it 'orders by products_count ascending' do
+ get :index, params: { q: { s: 'products_count asc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_one, vendor_two])
+ end
+
+ it 'orders by products_count descending' do
+ get :index, params: { q: { s: 'products_count desc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_two, vendor_one])
+ end
+
+ it 'orders by sales_total ascending' do
+ get :index, params: { q: { s: 'sales_total asc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_one, vendor_two])
+ end
+
+ it 'orders by sales_total descending' do
+ get :index, params: { q: { s: 'sales_total desc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_two, vendor_one])
+ end
+
+ it 'orders by commission_total ascending' do
+ get :index, params: { q: { s: 'commission_total asc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_one, vendor_two])
+ end
+
+ it 'orders by commission_total descending' do
+ get :index, params: { q: { s: 'commission_total desc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_two, vendor_one])
+ end
+
+ it 'orders by join date ascending' do
+ get :index, params: { q: { s: 'created_at asc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_one, vendor_two])
+ end
+
+ it 'orders by join date descending' do
+ get :index, params: { q: { s: 'created_at desc' } }
+ expect(assigns(:collection).to_a).to eq([vendor_two, vendor_one])
+ end
+
+ it 'should only give distinct vendor when searched with vendor name' do
+ get :index, params: { q: { name_eq: 'Vendor_one' } }
+ expect(assigns(:collection).to_a).to contain_exactly(vendor_one)
+ expect(assigns(:collection).to_a.count).to eq(1)
+ end
+
+ it 'should only give vendors with integration as shopify' do
+ get :index, params: { q: { integration_eq: 'shopify' } }
+ expect(assigns(:collection).to_a).to contain_exactly(vendor_one, vendor_two)
+ expect(assigns(:collection).to_a.count).to eq(2)
+ end
+
+ context 'CSV format' do
+ let(:format) { 'csv' }
+
+ before do
+ get :index, format: format
+ end
+
+ it 'renders CSV' do
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ describe '#update' do
+ subject(:update_vendor) do
+ post :update, params: { id: vendor.id, vendor: vendor_update_params }
+ end
+
+ let!(:vendor) { create(:vendor) }
+
+ context 'adds a returns address' do
+ let(:country) { Spree::Country.find_by(iso: 'US') }
+ let(:state) { create(:state, country: country, abbr: 'NY') }
+
+ let(:vendor_update_params) do
+ {
+ returns_address_attributes: {
+ city: 'New York',
+ country_id: country.id,
+ state_id: state.id,
+ zipcode: '10036',
+ address1: '123 Main St'
+ }
+ }
+ end
+
+ it 'creates a new returns address' do
+ expect { update_vendor }.to change { Spree::ReturnsAddress.count }.by(1)
+ vendor.reload
+ expect(vendor.returns_address.city).to eq('New York')
+ expect(vendor.returns_address.country_id).to eq(country.id)
+ expect(vendor.returns_address.state_id).to eq(state.id)
+ expect(vendor.returns_address.zipcode).to eq('10036')
+ expect(vendor.returns_address.address1).to eq('123 Main St')
+ end
+
+ context 'update existing address' do
+ let(:returns_address) { create(:returns_address, city: 'New York', country: country, state: state) }
+
+ before do
+ vendor.returns_address = returns_address
+ vendor.save!
+ end
+
+ let(:vendor_update_params) do
+ { returns_address_attributes: { id: vendor.returns_address.id, city: 'New Jersey' } }
+ end
+
+ it 'updates address' do
+ expect { update_vendor }.to change { vendor.returns_address.reload.city }.from('New York').to('New Jersey')
+ end
+ end
+ end
+ end
+
+ describe '#show' do
+ let(:vendor) { create(:approved_vendor) }
+ let(:vendor_user) { create(:vendor_user, vendor: vendor) }
+ let(:store_role) { create(:role_user, resource: store, user: vendor_user) }
+
+ before do
+ store_role
+ end
+
+ it 'renders show' do
+ get :show, params: { id: vendor.id }
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'assigns @vendor_users with roles for proper resource' do
+ get :show, params: { id: vendor.id }
+ expect(assigns(:vendor_users).first.role_users.size).to eq(1)
+ expect(assigns(:vendor_users).first.role_users.first).not_to eq(store_role)
+ end
+ end
+end
diff --git a/spec/factories/spree/vendor_factory.rb b/spec/factories/spree/vendor_factory.rb
deleted file mode 100644
index 4267e611..00000000
--- a/spec/factories/spree/vendor_factory.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-FactoryBot.define do
- factory :vendor, class: Spree::Vendor do
- name { FFaker::Company.name }
- about_us { 'About us...' }
- contact_us { 'Contact us...' }
-
- factory :active_vendor do
- name { 'Active vendor' }
- state { :active }
- end
-
- factory :active_vendor_list do
- name { "#{FFaker::Company.name} #{FFaker::Company.suffix }" }
- state { :active }
- end
-
-
- factory :pending_vendor do
- name { 'Pending vendor' }
- state { :pending }
- end
-
- factory :blocked_vendor do
- name { 'Blocked vendor' }
- state { :blocked }
- end
- end
-end
diff --git a/spec/factories/spree/vendor_image_factory.rb b/spec/factories/spree/vendor_image_factory.rb
deleted file mode 100644
index dc3b5538..00000000
--- a/spec/factories/spree/vendor_image_factory.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-FactoryBot.define do
- factory :vendor_image, class: Spree::VendorImage do
- attachment { Rack::Test::UploadedFile.new(SpreeMultiVendor::Engine.root.join('spec', 'fixtures', 'thinking-cat.jpg'), 'image/jpeg') }
- end
-end
diff --git a/spec/factories/spree/vendor_user_factory.rb b/spec/factories/spree/vendor_user_factory.rb
deleted file mode 100644
index 60459e51..00000000
--- a/spec/factories/spree/vendor_user_factory.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-FactoryBot.define do
- factory :vendor_user, class: Spree::Vendor do
- user
- vendor
- end
-end
diff --git a/spec/features/spree/admin/orders_spec.rb b/spec/features/spree/admin/orders_spec.rb
deleted file mode 100644
index 127ecb6a..00000000
--- a/spec/features/spree/admin/orders_spec.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-require 'spec_helper'
-
-RSpec.feature 'Admin Orders', :js do
- let(:store) { Spree::Store.default }
- let(:vendor) { create(:active_vendor, name: 'Vendor') }
- let(:vendor_2) { create(:active_vendor, name: 'Other vendor') }
- let(:order) { create(:order, ship_address: create(:address), bill_address: create(:address), store: store) }
- let(:order_2) { create(:order, store: store) }
- let!(:user) { create(:user, vendors: [vendor]) }
- let(:product) { create(:product_in_stock, vendor: vendor, stores:[store]) }
- let(:product_2) { create(:product_in_stock, vendor: vendor_2,stores:[store]) }
-
- before do
- create(:line_item, order: order, product: product)
- create(:line_item, order: order, product: product_2)
- order.create_proposed_shipments
- order.update(state: :complete, completed_at: Time.current)
- login_as(user, scope: :spree_user)
- end
-
- shared_examples 'shows order data' do
- it { expect(page).to have_text(order.number) }
- it { expect(page).to have_content(order.display_vendor_total(vendor).to_html) }
- end
-
- context 'index' do
- background do
- visit spree.admin_orders_path
- end
-
- it_behaves_like 'shows order data'
-
- it 'does not show non-vendor orders' do
- expect(page).not_to have_text(order_2.number)
- end
- end
-
- context 'cart' do
- background do
- visit spree.cart_admin_order_path(id: order.number)
- end
-
- it_behaves_like 'shows order data'
- end
-
- context 'customer info' do
- background do
- visit spree.admin_order_customer_path(order_id: order.number)
- end
-
- it_behaves_like 'shows order data'
-
- it 'shows customer information' do
- expect(page).to have_text(order.ship_address.full_name)
- expect(page).to have_text(order.ship_address.address1)
- expect(page).to have_text(order.ship_address.city)
-
- expect(page).to have_text(order.bill_address.full_name)
- expect(page).to have_text(order.bill_address.address1)
- expect(page).to have_text(order.bill_address.city)
-
- expect(page).not_to have_selector('input[type=text]')
- end
- end
-
- context 'edit' do
- background do
- visit spree.edit_admin_order_path(id: order.number)
- end
-
- it_behaves_like 'shows order data'
-
- scenario 'vendor can add his product' do
- select2_open label: 'Name or SKU'
- select2_search product.name, from: 'Name or SKU'
- select2_select product.name, from: 'Name or SKU', match: :first
- expect(page).to have_text(product.name)
- end
-
- scenario 'vendor cannot add other vendors product' do
- select2_open label: 'Name or SKU'
- select2_search product_2.name, from: 'Name or SKU'
- expect(page).not_to have_text(product_2.name)
- end
- end
-end
diff --git a/spec/features/spree/admin/product_spec.rb b/spec/features/spree/admin/product_spec.rb
deleted file mode 100644
index b5248086..00000000
--- a/spec/features/spree/admin/product_spec.rb
+++ /dev/null
@@ -1,177 +0,0 @@
-require 'spec_helper'
-
-RSpec.feature 'Admin Products', :js do
- let(:vendor) { create(:active_vendor) }
- let!(:user) { create(:user, vendors: [vendor]) }
- let!(:admin) { create(:admin_user) }
- let!(:option_type) { create(:option_type, name: 'Testing option') }
- let!(:option_value) { create(:option_value, option_type: option_type) }
- let!(:product) { create(:product, sku: 'Test1') }
- let!(:vendor_product) { create(:product, vendor: vendor, sku: 'Test2', option_types: [option_type]) }
-
- context 'for user with admin role' do
- before { login_as(admin, scope: :spree_user) }
-
- context 'index' do
- scenario 'displays all products' do
- visit spree.admin_products_path
- expect(page).to have_selector('tr', count: 3)
- end
- end
-
- context 'create product' do
- scenario 'creates new product with vendor id unassigned' do
- visit spree.admin_products_path
- click_link 'New Product'
- expect(current_path).to eq spree.new_admin_product_path
-
- fill_in 'product_name', with: 'Vendor product'
- fill_in 'product_price', with: 15
- select2 Spree::ShippingCategory.last.name, from: 'Shipping Categories'
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(current_path).to eq spree.edit_admin_product_path(Spree::Product.last)
- expect(Spree::Product.last.vendor_id).to eq nil
- end
- scenario 'creates new product with assigned vendor' do
- visit spree.admin_products_path
- click_link 'New Product'
- expect(current_path).to eq spree.new_admin_product_path
-
- fill_in 'product_name', with: 'Vendor product'
- fill_in 'product_price', with: 15
- select2 Spree::ShippingCategory.last.name, from: 'Shipping Categories'
- select2 'Active vendor', from: 'Vendor'
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(current_path).to eq spree.edit_admin_product_path(Spree::Product.last)
- expect(Spree::Product.last.vendor_id).to eq Spree::Vendor.last.id
- end
- end
-
- context 'edit product' do
- before(:each) do
- visit spree.edit_admin_product_path(product)
- expect(current_path).to eq spree.edit_admin_product_path(product)
- end
-
- scenario 'can update an existing product' do
- fill_in 'product_name', with: 'Testing edit'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- expect(page).to have_text 'Testing edit'
- end
-
- scenario 'can update product master price' do
- fill_in 'product_price', with: 123
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- product.reload
- expect(product.price).to eq 123
- end
-
- scenario 'can update product vendor' do
- expect(product.vendor).to eq nil
- select2 vendor.name, from: 'Vendor'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- product.reload
- expect(product.vendor).to eq vendor
- end
- end
-
- context 'create variant' do
- scenario 'creates new variant with vendor id assigned' do
- visit spree.admin_product_variants_path(vendor_product)
- click_link 'New Variant'
- select2 'S', from: 'Size'
- click_button 'Create'
- expect(page).to have_text 'successfully created!'
- expect(Spree::Variant.last.vendor_id).to eq vendor.id
- end
- end
- end
-
- context 'for user with vendor' do
- before(:each) do
- login_as(user, scope: :spree_user)
- visit spree.admin_products_path
- end
-
- context 'index' do
- scenario 'displays only vendor product' do
- expect(page).to have_selector('tr', count: 2)
- end
- end
-
- context 'create product' do
- scenario 'can create a new product' do
- click_link 'New Product'
- expect(current_path).to eq spree.new_admin_product_path
-
- fill_in 'product_name', with: 'Vendor product'
- fill_in 'product_price', with: 15
- select2 Spree::ShippingCategory.last.name, from: 'Shipping Categories'
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(current_path).to eq spree.edit_admin_product_path(Spree::Product.last)
- expect(Spree::Product.last.vendor_id).to eq vendor.id
- end
- end
-
- context 'edit product' do
- before(:each) do
- within_row(1) { click_icon :edit }
- expect(current_path).to eq spree.edit_admin_product_path(vendor_product)
- end
-
- scenario 'can update an existing product' do
- fill_in 'product_name', with: 'Testing edit'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- expect(page).to have_text 'Testing edit'
- end
-
- scenario 'can update product master price' do
- fill_in 'product_price', with: 123
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- vendor_product.reload
- expect(vendor_product.price).to eq 123
- end
-
- scenario 'shows validation error with blank name' do
- fill_in 'product_name', with: ''
- click_button 'Update'
- expect(page).to have_text 'Name can\'t be blank'
- end
- end
-
- context 'create product property' do
- scenario 'can create new product property' do
- visit spree.admin_product_product_properties_path(vendor_product)
- fill_in 'product[product_properties_attributes][0][property_name]', with: 'Testing edit'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- # expect(Spree::ProductProperty.last.property.vendor_id).to eq vendor.id
- end
- end
-
- context 'create variant' do
- scenario 'can create new variant' do
- visit spree.admin_product_variants_path(vendor_product)
- click_link 'New Variant'
- select2 'S', from: 'Size'
- click_button 'Create'
- expect(page).to have_text 'successfully created!'
- expect(Spree::Variant.last.option_values).to include option_value
- end
- end
- end
-end
diff --git a/spec/features/spree/admin/shipping_method_spec.rb b/spec/features/spree/admin/shipping_method_spec.rb
deleted file mode 100644
index ce54f3c0..00000000
--- a/spec/features/spree/admin/shipping_method_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-require 'spec_helper'
-
-RSpec.feature 'Admin Shipping Methods', :js do
- let(:active_vendor) { create(:active_vendor) }
- let!(:user) { create(:user, vendors: [active_vendor]) }
- let!(:admin) { create(:admin_user) }
- let!(:shipping_method) { create(:shipping_method, name: 'Test') }
- let!(:vendor_shipping_method) { create(:shipping_method, name: 'Test', vendor: active_vendor) }
-
- context 'for user with admin role' do
- context 'index' do
- scenario 'displays all shipping methods' do
- login_as(admin, scope: :spree_user)
- visit spree.admin_shipping_methods_path
- expect(page).to have_selector('tr', count: 3)
- end
- end
- end
-
- context 'for user with vendor' do
- before(:each) do
- login_as(user, scope: :spree_user)
- visit spree.admin_shipping_methods_path
- end
-
- context 'index' do
- scenario 'displays only vendor shipping method' do
- expect(page).to have_selector('tr', count: 2)
- end
- end
-
- context 'create' do
- scenario 'can create a new shipping method' do
- click_link 'New Shipping Method'
- expect(current_path).to eq spree.new_admin_shipping_method_path
-
- fill_in 'shipping_method_name', with: 'Vendor shipping method'
- check Spree::ShippingCategory.last.name
-
- if Spree.version.to_f >= 4.0
- select2 'Both', from: 'Display'
- end
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(current_path).to eq spree.edit_admin_shipping_method_path(Spree::ShippingMethod.last)
- expect(Spree::ShippingMethod.last.vendor_id).to eq active_vendor.id
- end
- end
-
- context 'edit' do
- before(:each) do
- within_row(1) { click_icon :edit }
- expect(current_path).to eq spree.edit_admin_shipping_method_path(active_vendor.shipping_methods.first)
- end
-
- scenario 'can update an existing shipping method' do
- fill_in 'shipping_method_name', with: 'Testing edit'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- expect(page).to have_text 'Testing edit'
- end
-
- scenario 'shows validation error with blank name' do
- fill_in 'shipping_method_name', with: ''
- click_button 'Update'
- expect(page).to_not have_text 'successfully created!'
- end
- end
- end
-end
diff --git a/spec/features/spree/admin/stock_location_spec.rb b/spec/features/spree/admin/stock_location_spec.rb
deleted file mode 100644
index 5d00c6a9..00000000
--- a/spec/features/spree/admin/stock_location_spec.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-require 'spec_helper'
-
-RSpec.feature 'Admin Stock Locations', :js do
- let(:active_vendor) { create(:active_vendor) }
- let!(:product) { create(:product, vendor_id: active_vendor.id, name: 'Test') }
- let!(:user) { create(:user, vendors: [active_vendor]) }
- let!(:admin) { create(:admin_user) }
- let!(:stock_location) { create(:stock_location, name: 'Test') }
-
- context 'for user with admin role' do
- context 'index' do
- scenario 'displays all stock locations' do
- login_as(admin, scope: :spree_user)
- visit spree.admin_stock_locations_path
- expect(page).to have_selector('tr', count: 3)
- end
- end
- end
-
- context 'for user with vendor' do
- before(:each) do
- login_as(user, scope: :spree_user)
- visit spree.admin_stock_locations_path
- end
-
- context 'index' do
- scenario 'displays only vendor stock location' do
- expect(page).to have_selector('tr', count: 2)
- end
- end
-
- context 'stock movements' do
- scenario 'displays stock movements for vendor stock location' do
- click_on 'Stock Movements'
- expect(page).to have_text 'Stock Movements for Active vendor'
- end
-
- scenario 'can create a new stock movement for vendor stock location' do
- click_on 'Stock Movements'
- click_on 'New Stock Movement'
- expect(current_path).to eq spree.new_admin_stock_location_stock_movement_path(active_vendor.stock_locations.first)
-
- fill_in 'stock_movement_quantity', with: 5
- select2_open label: 'Stock Item'
- select2_search product.name, from: 'Stock Item'
- select2_select product.name, from: 'Stock Item', match: :first
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(Spree::StockMovement.count).to eq 1
- end
- end
-
- context 'create' do
- scenario 'can create a new stock location' do
- click_link 'New Stock Location'
- expect(current_path).to eq spree.new_admin_stock_location_path
-
- fill_in 'stock_location_name', with: 'Vendor stock location'
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(current_path).to eq spree.admin_stock_locations_path
- expect(Spree::StockLocation.last.vendor_id).to eq active_vendor.id
- end
- end
-
- context 'edit' do
- before(:each) do
- within_row(1) { click_icon :edit }
- expect(current_path).to eq spree.edit_admin_stock_location_path(active_vendor.stock_locations.first)
- end
-
- scenario 'can update an existing stock location' do
- fill_in 'stock_location_name', with: 'Testing edit'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- expect(page).to have_text 'Testing edit'
- end
-
- scenario 'shows validation error with blank name' do
- fill_in 'stock_location_name', with: ''
- click_button 'Update'
- expect(page).to_not have_text 'successfully created!'
- end
- end
- end
-end
diff --git a/spec/features/spree/admin/vendor_settings_spec.rb b/spec/features/spree/admin/vendor_settings_spec.rb
deleted file mode 100644
index 067f3e19..00000000
--- a/spec/features/spree/admin/vendor_settings_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-RSpec.feature 'Admin Vendor Settings', :js do
- let(:vendor) { create(:active_vendor) }
- let(:blocked_vendor) { create(:blocked_vendor)}
- let!(:user) { create(:user, vendors: [vendor]) }
- let!(:user_with_blocked_vendor) { create(:user, vendors: [blocked_vendor]) }
-
- context 'edit' do
- background do
- login_as(user, scope: :spree_user)
- visit spree.admin_vendor_settings_path
- end
-
- scenario 'can update current vendor' do
- fill_in 'vendor_name', with: 'Testing edit'
- fill_in 'vendor_about_us', with: 'Testing about us edit'
- fill_in 'vendor_contact_us', with: 'Testing contact us edit'
- expect {
- click_button 'Update'
- }.to change { vendor.reload.slug }.from('active-vendor').to('testing-edit')
- expect(page).to have_text 'Testing edit'
- expect(page).to have_text 'Testing about us edit'
- expect(page).to have_text 'Testing contact us edit'
- end
-
- scenario 'shows validation error with blank name' do
- fill_in 'vendor_name', with: ''
- click_button 'Update'
- expect(page).to have_text 'name can\'t be blank'
- end
-
- scenario 'does not show validation error with blank about_us' do
- fill_in 'vendor_about_us', with: ''
- click_button 'Update'
- expect(page).not_to have_text 'about_us can\'t be blank'
- end
-
- scenario 'does not show validation error with blank contact_us' do
- fill_in 'vendor_contact_us', with: ''
- click_button 'Update'
- expect(page).not_to have_text 'contact_us can\'t be blank'
- end
-
- scenario 'can update an existing vendor image' do
- create(:vendor, name: 'New vendor')
- page.attach_file("vendor_image", Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg')
- click_button 'Update'
- expect(page).to have_css("img[src*='thinking-cat.jpg']")
- end
- end
-
- context 'no vendor present' do
- before do
- vendor.destroy!
- end
-
- it 'renders page without errors' do
- visit spree.admin_vendor_settings_path
- expect(page).to have_current_path('/admin/login')
- end
- end
-
- context 'blocked vendor present' do
- background do
- login_as(user_with_blocked_vendor, scope: :spree_user)
- end
-
- it 'renders forbidden page' do
- visit spree.admin_vendor_settings_path
- expect(current_path).to eq spree.admin_forbidden_path
- end
- end
-end
diff --git a/spec/features/spree/admin/vendor_spec.rb b/spec/features/spree/admin/vendor_spec.rb
deleted file mode 100644
index 241ede16..00000000
--- a/spec/features/spree/admin/vendor_spec.rb
+++ /dev/null
@@ -1,125 +0,0 @@
-require 'spec_helper'
-
-RSpec.feature 'Admin Vendors', :js do
- let!(:admin) { create(:admin_user) }
- let!(:vendor_user) { create(:user) }
-
- background do
- login_as(admin, scope: :spree_user)
- create(:active_vendor, users: [vendor_user]) if Spree::Vendor.find_by(name: 'Active vendor').nil?
- create(:pending_vendor)
- create(:blocked_vendor)
- visit spree.admin_vendors_path
- end
-
- context 'index' do
- scenario 'displays existing vendors' do
- within_row(1) do
- expect(column_text(1)).to eq vendor_user.email
- expect(column_text(2)).to eq 'Active vendor'
- expect(column_text(3)).to eq 'active'
- end
- end
-
- scenario 'filter by multiple states' do
- click_button 'Filter'
- select2 'Active', from: 'State'
- select2 'Pending', from: 'State'
-
- click_button 'Filter Results'
-
- expect(page).to have_content('active')
- expect(page).to have_content('pending')
- expect(page).not_to have_content('blocked')
- end
- end
-
- context 'create' do
- scenario 'can create a new vendor' do
- click_link 'New Vendor'
- expect(current_path).to eq spree.new_admin_vendor_path
-
- fill_in 'vendor_name', with: 'Test'
- fill_in 'vendor_about_us', with: 'About...'
- fill_in 'vendor_contact_us', with: 'Contact...'
- expect(find_field('vendor_commission_rate').value).to eq '5.0'
- select 'Blocked'
-
- click_button 'Create'
-
- expect(page).to have_text 'successfully created!'
- expect(current_path).to eq spree.admin_vendors_path
- end
-
- scenario 'shows validation error with blank name' do
- click_link 'New Vendor'
- expect(current_path).to eq spree.new_admin_vendor_path
-
- fill_in 'vendor_name', with: ''
- click_button 'Create'
-
- expect(page).to have_text 'name can\'t be blank'
- end
-
- scenario 'shows validation error with repeated name' do
- click_link 'New Vendor'
- expect(current_path).to eq spree.new_admin_vendor_path
-
- fill_in 'vendor_name', with: 'Active vendor'
- click_button 'Create'
-
- expect(page).to have_text 'name has already been taken'
- end
- end
-
- context 'edit' do
- background do
- within_row(1) { click_icon :edit }
- expect(current_path).to eq spree.edit_admin_vendor_path('active-vendor')
- end
-
- scenario 'can update an existing vendor' do
- fill_in 'vendor_name', with: 'Testing edit'
- fill_in 'vendor_about_us', with: 'Testing about us'
- fill_in 'vendor_contact_us', with: 'Testing contact us'
- fill_in 'vendor_commission_rate', with: '5.5'
- click_button 'Update'
- expect(page).to have_text 'successfully updated!'
- expect(page).to have_text 'Testing edit'
- end
-
- if Spree.version.to_f >= 3.6
- scenario 'can update an existing vendor image' do
- page.attach_file("vendor_image", Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg')
- expect { click_button 'Update' }.to change(Spree::VendorImage, :count).by(1)
- expect(page).to have_text 'successfully updated!'
- end
- end
-
- scenario 'shows validation error with blank name' do
- fill_in 'vendor_name', with: ''
- click_button 'Update'
- expect(page).to have_text 'name can\'t be blank'
- end
-
- scenario 'does not show validation error with blank about_us' do
- fill_in 'vendor_about_us', with: ''
- click_button 'Update'
- expect(page).not_to have_text 'about_us can\'t be blank'
- end
-
- scenario 'does not show validation error with blank contact_us' do
- fill_in 'vendor_contact_us', with: ''
- click_button 'Update'
- expect(page).not_to have_text 'contact_us can\'t be blank'
- end
-
- scenario 'shows validation error with repeated name' do
- create(:vendor, name: 'New vendor')
-
- fill_in 'vendor_name', with: 'New vendor'
- click_button 'Update'
- expect(page).to have_text 'name has already been taken'
- end
- end
-end
diff --git a/spec/fixtures/files/icon_256x256.jpg b/spec/fixtures/files/icon_256x256.jpg
new file mode 100644
index 00000000..2edee4a6
Binary files /dev/null and b/spec/fixtures/files/icon_256x256.jpg differ
diff --git a/spec/fixtures/thinking-cat.jpg b/spec/fixtures/thinking-cat.jpg
deleted file mode 100644
index 7e8524d3..00000000
Binary files a/spec/fixtures/thinking-cat.jpg and /dev/null differ
diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb
new file mode 100644
index 00000000..eacd60d0
--- /dev/null
+++ b/spec/i18n_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'i18n/tasks'
+
+RSpec.describe I18n do
+ let(:i18n) { I18n::Tasks::BaseTask.new }
+ let(:missing_keys) { i18n.missing_keys }
+ let(:inconsistent_interpolations) { i18n.inconsistent_interpolations }
+
+ it 'does not have missing keys' do
+ expect(missing_keys).to be_empty,
+ "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
+ end
+
+ it 'files are normalized' do
+ non_normalized = i18n.non_normalized_paths
+ error_message = "The following files need to be normalized:\n" \
+ "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
+ "Please run `i18n-tasks normalize' to fix"
+ expect(non_normalized).to be_empty, error_message
+ end
+
+ it 'does not have inconsistent interpolations' do
+ error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
+ "Run `i18n-tasks check-consistent-interpolations' to show them"
+ expect(inconsistent_interpolations).to be_empty, error_message
+ end
+end
diff --git a/spec/mailers/vendor_mailer_spec.rb b/spec/mailers/vendor_mailer_spec.rb
index 537f513c..8002d62a 100644
--- a/spec/mailers/vendor_mailer_spec.rb
+++ b/spec/mailers/vendor_mailer_spec.rb
@@ -1,45 +1,131 @@
require 'spec_helper'
+require 'email_spec'
-describe Spree::VendorMailer, type: :mailer do
- let(:store) { create(:store)}
- let(:vendor) { Spree::Vendor.new(id: 1, name: 'Test vendor', state: 'active', notification_email: 'test-vendor@example.com') }
- let(:other_vendor) { Spree::Vendor.new(id: 2, name: 'Other vendor', state: 'active', notification_email: 'other-vendor@example.com') }
- let(:order) { create(:completed_order_with_totals, line_items_count: 5) }
+RSpec.describe Spree::VendorMailer, type: :mailer do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
- context 'vendor notification email' do
- before do
- order.line_items.first.product.update(vendor: vendor)
- order.line_items.last.product.update(vendor: other_vendor)
+ let!(:store) { Spree::Store.default }
+ let!(:marketplace_owner) { create(:admin_user) }
+ let!(:vendor) { create(:vendor, invited_by: marketplace_owner) }
+ let!(:vendor_owner) { create(:vendor_user, vendor: vendor) }
+
+ describe '#vendor_suspended' do
+ let(:vendor_suspended_mail) { described_class.vendor_suspended(vendor) }
+
+ it 'has Vendor Owner email as to mail' do
+ expect(vendor_suspended_mail.to).to include(vendor_owner.email)
+ end
+
+ it 'contains Store name and Vendor name in subject' do
+ expect(vendor_suspended_mail.subject).to include(store.name)
+ expect(vendor_suspended_mail.subject).to include(vendor.name)
end
- it 'is sent from current store email address' do
- notification_email = Spree::VendorMailer.vendor_notification_email(order.id, vendor.id)
- expect(notification_email.from).to have_content(Spree::Store.current.mail_from_address)
+ it 'has the correct message in the body' do
+ message = 'If you would like to discuss this matter please contact' + \
+ " us via email #{store.mail_from_address}"
+
+ expect(vendor_suspended_mail).to have_body_text("We're sorry but your organization was suspended.")
+ expect(vendor_suspended_mail).to have_body_text(message)
+ end
+ end
+
+ describe '#vendor_approved' do
+ describe 'email sent to vendor owners' do
+ subject(:vendor_approved_mail) { described_class.with(vendor: vendor, recipient: vendor_owner).vendor_approved }
+
+ it 'has Vendor Owner email as to mail' do
+ expect(vendor_approved_mail.to).to include(vendor_owner.email)
+ end
+
+ it 'contains Store name and Vendor name in subject' do
+ expect(vendor_approved_mail.subject).to include(store.name)
+ expect(vendor_approved_mail.subject).to include(vendor.name)
+ end
+
+ it 'has the correct message in the body' do
+ expect(vendor_approved_mail).to have_body_text(spree.admin_dashboard_path)
+ end
end
- it 'is sent to vendors email address' do
- notification_email = Spree::VendorMailer.vendor_notification_email(order.id, vendor.id)
- expect(notification_email.to).to have_content(vendor.contact_us)
+ describe 'email sent to marketplace owners' do
+ subject(:vendor_approved_mail) do
+ described_class.with(
+ vendor: vendor,
+ recipient: marketplace_owner
+ ).vendor_approved
+ end
+
+ it 'has marketplace owner email as to mail' do
+ expect(vendor_approved_mail.to).to include(marketplace_owner.email)
+ end
+
+ it 'has valid subject' do
+ expect(vendor_approved_mail.subject).to eq "#{vendor.name} has been approved to start selling on #{vendor.store.name}!"
+ end
+
+ it 'has the correct message in the body' do
+ expect(vendor_approved_mail).to have_body_text "#{vendor.name} has been approved to start selling on #{vendor.store.name}!"
+ expect(vendor_approved_mail).to have_body_text(spree.admin_vendor_url(vendor, host: vendor.store.url))
+ end
end
+ end
+
+ describe '#vendor_rejected' do
+ let(:vendor_rejected_mail) { described_class.vendor_rejected(vendor) }
- it 'subject contains order number' do
- notification_email = Spree::VendorMailer.vendor_notification_email(order.id, vendor.id)
- expect(notification_email.subject).to have_content(order.number)
+ it 'has Vendor Owner email as to mail' do
+ expect(vendor_rejected_mail.to).to include(vendor_owner.email)
end
- it 'body contains vendors items' do
- notification_email = Spree::VendorMailer.vendor_notification_email(order.id, vendor.id)
- vendors_item = order.line_items.select { |line_item| line_item.product.vendor == vendor }.first
- expect(notification_email.body.parts.first).to have_text(vendors_item.product.name)
- expect(notification_email.body.parts.last).to have_text(vendors_item.product.name)
+ it 'contains Store name and Vendor name in subject' do
+ expect(vendor_rejected_mail.subject).to include(store.name)
+ expect(vendor_rejected_mail.subject).to include(vendor.name)
end
- it 'body does not contain other vendors items' do
- notification_email = Spree::VendorMailer.vendor_notification_email(order.id, vendor.id)
- other_vendors_item = order.line_items.select { |line_item| line_item.product.vendor != vendor }.first
- expect(notification_email.body.parts.first).not_to have_text(other_vendors_item.product.name)
- expect(notification_email.body.parts.last).not_to have_text(other_vendors_item.product.name)
+ it 'has the correct message in the body' do
+ expect(vendor_rejected_mail).to have_body_text(store.mail_from_address)
end
end
+ describe '#vendor_onboarding_started' do
+ let(:vendor_onboarding_started_mail) { described_class.with(vendor: vendor, recipient: marketplace_owner).vendor_onboarding_started }
+
+ context 'to mail' do
+ it 'has a Store Owners emails as to mail' do
+ expect(vendor_onboarding_started_mail.to).to eq([marketplace_owner.email])
+ end
+ end
+
+ it 'contains Vendor name and link to Vendor profile in subject' do
+ expect(vendor_onboarding_started_mail.subject).to include(vendor.name)
+ expect(vendor_onboarding_started_mail.subject).to include('started the onboarding process')
+ end
+
+ it 'has the correct message in the body' do
+ expect(vendor_onboarding_started_mail).to have_body_text(vendor.name)
+ expect(vendor_onboarding_started_mail).to have_body_text(spree.admin_vendor_url(vendor, host: vendor.store.url))
+ end
+ end
+
+ describe '#vendor_onboarding_completed' do
+ let(:vendor_onboarding_completed_mail) { described_class.with(vendor: vendor, recipient: marketplace_owner).vendor_onboarding_completed }
+
+ context 'to mail' do
+ it 'has a Store Owners emails as to mail' do
+ expect(vendor_onboarding_completed_mail.to).to eq([marketplace_owner.email])
+ end
+ end
+
+ it 'contains Vendor name and link to Vendor profile in subject' do
+ expect(vendor_onboarding_completed_mail).to have_subject(/#{vendor.name}/)
+ expect(vendor_onboarding_completed_mail).to have_subject(/completed their onboarding/)
+ end
+
+ it 'has the correct message in the body' do
+ expect(vendor_onboarding_completed_mail).to have_body_text(vendor.name)
+ expect(vendor_onboarding_completed_mail).to have_body_text(spree.admin_vendor_url(vendor, host: vendor.store.url))
+ end
+ end
end
diff --git a/spec/models/spree/calculators/returns/platform_refund_amount_spec.rb b/spec/models/spree/calculators/returns/platform_refund_amount_spec.rb
new file mode 100644
index 00000000..9b219519
--- /dev/null
+++ b/spec/models/spree/calculators/returns/platform_refund_amount_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Calculators::Returns::PlatformRefundAmount do
+ let(:calculator) { described_class.new }
+
+ describe '#compute' do
+ subject { calculator.compute(return_item) }
+
+ let(:return_item) { create(:return_item, inventory_unit: inventory_unit) }
+ let(:inventory_unit) { create(:inventory_unit, quantity: return_quantity, line_item: line_item) }
+
+ let(:line_item) do
+ create(:line_item,
+ quantity: 3,
+ price: 100,
+ platform_fee_per_unit: 10.12)
+ end
+
+ let(:exchange_variant) { nil }
+ let(:return_quantity) { 3 }
+ let(:promo_total) { 0 }
+ let(:included_tax_total) { 0 }
+
+ before do
+ line_item.update!(
+ promo_total: promo_total,
+ included_tax_total: included_tax_total
+ )
+ end
+
+ context 'for a full refund' do
+ it { is_expected.to eq(269.64) }
+
+ context 'when discounted' do
+ let(:promo_total) { 9.24 }
+
+ it { is_expected.to eq(260.4) }
+ end
+
+ context 'when there is included tax' do
+ let(:included_tax_total) { 9.24 }
+
+ it { is_expected.to eq(260.4) }
+ end
+
+ context 'when both discounted and there is included tax' do
+ let(:promo_total) { 9.24 }
+ let(:included_tax_total) { 9.24 }
+
+ it { is_expected.to eq(251.16) }
+ end
+
+ context 'when there is no platform fee' do
+ before do
+ line_item.update!(
+ platform_fee_per_unit: 0.00,
+ platform_fee_total: 0.00,
+ platform_fee_rate: 0.00
+ )
+ end
+
+ it { is_expected.to eq(300.00) }
+ end
+ end
+
+ context 'for a partial refund' do
+ let(:return_quantity) { 2 }
+
+ it { is_expected.to eq(179.76) }
+
+ context 'when discounted' do
+ let(:promo_total) { 9.24 }
+
+ it { is_expected.to eq(173.6) }
+ end
+
+ context 'when there is included tax' do
+ let(:included_tax_total) { 9.24 }
+
+ it { is_expected.to eq(173.6) }
+ end
+
+ context 'when both discounted and there is included tax' do
+ let(:promo_total) { 9.24 }
+ let(:included_tax_total) { 9.24 }
+
+ it { is_expected.to eq(167.44) }
+ end
+
+ context 'when there is no platform fee' do
+ before do
+ line_item.update!(
+ platform_fee_per_unit: 0.00,
+ platform_fee_total: 0.00,
+ platform_fee_rate: 0.00
+ )
+ end
+
+ it { is_expected.to eq(200.00) }
+ end
+ end
+
+ context 'when return is an exchange' do
+ let(:return_item) { double(:return_item, exchange_requested?: true) }
+
+ it { is_expected.to eq(0) }
+ end
+ end
+end
diff --git a/spec/models/spree/invitation_spec.rb b/spec/models/spree/invitation_spec.rb
new file mode 100644
index 00000000..2653056d
--- /dev/null
+++ b/spec/models/spree/invitation_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Invitation, type: :model do
+ let!(:store) { create(:store, default: true) }
+ let(:invitation) { create(:invitation) }
+
+ before do
+ clear_enqueued_jobs
+ end
+
+ describe 'State Machine' do
+ it 'has initial state of pending' do
+ expect(invitation.status).to eq('pending')
+ end
+
+ context 'when accepting an invitation' do
+ before do
+ invitation.invitee = create(:admin_user, :without_admin_role)
+ end
+
+ context 'when the resource is a vendor' do
+ it 'starts onboarding' do
+ invitation.resource = create(:invited_vendor)
+ invitation.accept!
+ expect(invitation.resource.reload.onboarding?).to be true
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb
index fa6e7273..6eb8d316 100644
--- a/spec/models/spree/line_item_spec.rb
+++ b/spec/models/spree/line_item_spec.rb
@@ -1,32 +1,50 @@
require 'spec_helper'
-describe Spree::LineItem do
- let(:vendor) { create(:vendor) }
-
- describe '#vendor' do
- context 'vendorized product' do
- let(:product) { create(:product, vendor: vendor) }
- let(:line_item) { build(:line_item, product: product, variant: product.default_variant) }
-
- it { expect(line_item.vendor).not_to be_nil }
- it { expect(line_item.vendor).to eq(product.vendor) }
+RSpec.describe Spree::LineItem, type: :model do
+ context 'vendorized line item' do
+ it 'assigns vendor from product' do
+ product = create(:product_in_stock, vendor: create(:approved_vendor))
+ line_item = create(:line_item, product: product)
+ expect(line_item.vendor).to eq(product.vendor)
end
+ end
- context 'vendorized variant' do
- let(:product) { create(:product) }
- let(:variant) { create(:variant, vendor: vendor, product: product) }
- let(:line_item) { build(:line_item, product: product, variant: variant) }
+ context 'scope' do
+ context 'platform_feeable' do
+ subject { described_class.platform_feeable }
+
+ let(:product) { create(:product_in_stock, vendor: vendor) }
+ let(:vendor) { create(:approved_vendor) }
+ let(:line_item) { create(:line_item, product: product) }
before do
- SpreeMultiVendor::Config[:vendorized_models] = ['variant']
+ line_item.update_column(:pre_tax_amount, pre_tax_amount)
+ end
+
+ context 'if item belongs to the vendor and has positive pre_tax_amount' do
+ let(:pre_tax_amount) { 1.0 }
+
+ it 'returns an item' do
+ expect(subject).to eq [line_item]
+ end
end
- after do
- SpreeMultiVendor::Config[:vendorized_models] = SpreeMultiVendor::Configuration::DEFAULT_VENDORIZED_MODELS
+ context 'if item belongs to the vendor, but has zero pretax_amount' do
+ let(:pre_tax_amount) { 0.0 }
+
+ it 'does not return the item' do
+ expect(subject).to be_empty
+ end
end
- it { expect(line_item.vendor).not_to be_nil }
- it { expect(line_item.vendor).to eq(variant.vendor) }
+ context 'if does not belong to the vendor, but has positive pretax_amount' do
+ let(:pre_tax_amount) { 1.0 }
+ let(:vendor) { nil }
+
+ it 'does not return the item' do
+ expect(subject).to be_empty
+ end
+ end
end
end
end
diff --git a/spec/models/spree/order_commission_spec.rb b/spec/models/spree/order_commission_spec.rb
deleted file mode 100644
index dce1bc8f..00000000
--- a/spec/models/spree/order_commission_spec.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'spec_helper'
-
-describe Spree::OrderCommission do
- describe 'associations' do
- it { is_expected.to belong_to(:order) }
- it { is_expected.to belong_to(:vendor) }
- end
-end
diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb
index 7e61ae7f..dfd70042 100644
--- a/spec/models/spree/order_spec.rb
+++ b/spec/models/spree/order_spec.rb
@@ -1,193 +1,505 @@
require 'spec_helper'
-describe Spree::Order do
- let(:vendor) { create(:vendor, name: 'Vendor 1') }
- let(:vendor_2) { create(:vendor, name: 'Vendor 2') }
-
- let(:order) { create(:order_with_line_items, state: 'payment', line_items_count: 6, line_items_price: 100, shipment_cost: 50) }
+class FakeCalculator < Spree::Calculator
+ def compute(_computable)
+ 5
+ end
+end
+RSpec.describe Spree::Order, type: :model do
before do
- order.line_items.each_with_index do |li, idx|
- product = li.product
- product.vendor = idx.odd? ? vendor : vendor_2
- product.save
- end
+ allow(Spree::LegacyUser).to receive_messages(current: create(:user))
end
- context 'associations' do
- it { is_expected.to have_many(:commissions) }
+ let(:user) { create(:user) }
+ let(:store) { Spree::Store.default }
+ let(:order) { create(:order, user: user, store: store) }
+
+ describe 'vendors association' do
+ let(:vendor) { create(:vendor) }
+ let(:product) { create(:product_in_stock, vendor: vendor, price: 100.0) }
+ let(:variant) { product.default_variant }
+ let!(:line_item) { create_list(:line_item, 2, order: order, product: product, price: product.price) }
+
+ it do
+ # Regression test for ActiveRecord::StatementInvalid: PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
+ expect { order.vendors }.not_to raise_error(ActiveRecord::StatementInvalid)
+ expect(order.vendors.to_a).to match_array([vendor])
+ end
end
- context 'complete with spree_multi_vendor' do
- before { allow(order).to receive_messages payment_required?: false }
+ describe 'callbacks' do
+ describe 'after_complete' do
+ let(:order) { create(:order_with_totals, state: :confirm, total: 100) }
- it 'generates commission records' do
- expect do
- order.next!
- order.reload
- end.to change { Spree::OrderCommission.count }.by(2).and change { vendor.commissions.count }.by(1)
- .and change { vendor_2.commissions.count }.by(1)
+ describe '#split_order' do
+ subject(:complete_order) { order.next! }
+
+ before do
+ allow(order).to receive(:process_payments!)
+ end
+
+ context 'for an order with 1st party products' do
+ let(:order) { create(:placed_order) }
+
+ it 'skips splitting the order after completing it' do
+ expect { complete_order }.to_not have_enqueued_job(SpreeMultiVendor::Orders::SplitByVendorJob)
+ end
+ end
+
+ context 'for an order with 3rd party products' do
+ let(:order) { create(:placed_order_with_vendor_items) }
+
+ it 'splits order after completing it' do
+ expect { complete_order }.to have_enqueued_job(SpreeMultiVendor::Orders::SplitByVendorJob).with(order.id)
+ end
+ end
+ end
+ end
- order.line_items.includes(product: :vendor).group_by { |li| li.product.vendor }.each do |v, line_items|
- commission_ammount = v.commission_rate * line_items.pluck(:pre_tax_amount).sum.to_f / 100
+ describe 'touches parent order on update' do
+ let!(:parent_order) { create(:splitted_order) }
+ let!(:order) { create(:vendor_order_ready_to_ship, parent: parent_order) }
- expect(v.commissions.sum(:amount)).to eq(commission_ammount)
+ it 'touches parent order' do
+ expect { order.touch }.to change { parent_order.reload.updated_at }
end
end
+ end
+
+ describe '#cancel' do
+ # this test should be improved with real data and VCRs
+ context 'when splitted' do
+ let(:payment_method) { create(:credit_card_payment_method, stores: [store]) }
+
+ let!(:parent_order) { create(:splitted_order, shipment_cost: 5) }
+ # setting shipment_cost to 0 to avoid shipment cost calculation and for easier checking if refunds amounts are correct as we do
+ # not refund shipment cost
+ let!(:order) { create(:vendor_order_ready_to_ship, with_payment: false, store: store, parent: parent_order, shipment_cost: 0, total: 100, payment_total: 0) }
+
+ let(:payment) do
+ create(
+ :payment,
+ order: order,
+ amount: 100,
+ state: 'completed',
+ payment_method: payment_method
+ )
+ end
+
+ before do
+ # Just to speed up the test, we don't care about the email anyway
+ allow(Spree::OrderMailer).to receive(:cancel_email).and_return(double(deliver_later: true))
+ order.update_column(:total, 110)
+ payment
+ end
+
+ context 'single sub-order without 1st party products' do
+ before do
+ parent_order.line_items.destroy_all
+
+ order.cancel
+ parent_order.reload
+ order.reload
+ end
+
+ it 'marks the splitted payments as void' do
+ expect(order.payments.first).to be_void
+ end
+
+ it 'marks the parent order and sub-order both as canceled' do
+ expect(parent_order.reload).to be_canceled
+ expect(parent_order.payment_state).to eq 'void'
+ expect(parent_order.shipment_state).to eq 'canceled'
+ expect(order).to be_canceled
+ end
+
+ it 'marks the splitted order payment status as failed' do
+ expect(order.payment_state).to eq 'failed'
+ end
+ end
+
+ context 'refunds' do
+ it 'creates a refund for the sub-order' do
+ expect { order.cancel }.to change { Spree::Refund.count }.by(2)
- it 'sends mails only once per vendor' do
- mail_double_1 = double(:mail_double_1)
- mail_double_2 = double(:mail_double_1)
+ expect(order.reload.refunds.count).to eq 1
+ expect(order.refunds.first.amount).to eq 100
+ expect(order.refunds.first.payment).to eq order.payments.first
- expect(Spree::VendorMailer).to receive(:vendor_notification_email)
- .with(be_an(Integer), vendor.id) { mail_double_1 }
- expect(mail_double_1).to receive(:deliver_later)
- expect(Spree::VendorMailer).to receive(:vendor_notification_email)
- .with(be_an(Integer), vendor_2.id) { mail_double_2 }
- expect(mail_double_2).to receive(:deliver_later)
+ expect(parent_order.reload.refunds.count).to eq 1
+ expect(parent_order.refunds.first.amount).to eq(order.total + order.platform_fee_total.abs)
+ end
- order.next!
+ it 'creates a refund for the parent order' do
+ order.cancel!
+
+ expect(parent_order.reload.refunds.count).to eq(1)
+ expect(parent_order.refunds.last.amount).to eq(order.total + order.platform_fee_total.abs)
+ end
+ end
+
+ context 'multiple sub-orders' do
+ let!(:order_2) { create(:vendor_order_ready_to_ship, store: store, parent: parent_order) }
+
+ context 'only 1 sub-order is canceled' do
+ it 'marks the parent order as partially canceled' do
+ order.cancel
+ parent_order.reload
+ expect(parent_order.reload.state).to eq('partially_canceled')
+ end
+ end
+
+ context 'all sub-orders are canceled' do
+ before do
+ parent_order.line_items.destroy_all
+
+ parent_order.payments.update_all(state: 'completed', amount: 200)
+ parent_order.update_columns(shipment_total: 20)
+ end
+
+ it 'marks the parent order as canceled' do
+ order.cancel
+ expect(parent_order.reload.state).to eq('partially_canceled')
+ order_2.cancel
+ parent_order.reload
+ expect(parent_order.reload).to be_canceled
+ expect(parent_order.payment_state).to eq 'void'
+ expect(parent_order.shipment_state).to eq 'canceled'
+ end
+
+ it 'refunds shipment cost' do
+ order.cancel
+ order_2.cancel
+
+ expect(parent_order.refunds.count).to eq(2)
+ expect(parent_order.refunds.map(&:amount).uniq).to contain_exactly(order.total + order.platform_fee_total.abs)
+ expect(parent_order.refunds.map(&:amount).uniq).to contain_exactly(order_2.total + order_2.platform_fee_total.abs)
+ end
+ end
+ end
end
end
- context 'vendor methods' do
- let!(:shipment) { create(:shipment, order: order, cost: 20, pre_tax_amount: 20, stock_location: vendor.stock_locations.first) }
- let!(:promotion) { create(:promotion_with_item_adjustment, adjustment_rate: 10, code: '10off') }
+ describe '#persist_totals' do
+ let(:order) { create(:order_with_line_items) }
- before do
- order.coupon_code = '10off'
- Spree::PromotionHandler::Coupon.new(order).apply
- order.reload
+ it 'updates the platform fee total' do
+ expect(order).to receive(:update_column).with(:platform_fee_total, order.platform_fee_total)
+ order.persist_totals
end
+ end
- describe '#vendor_line_items' do
- it { expect(order.vendor_line_items(vendor)).to eq(order.line_items.for_vendor(vendor)) }
- it { expect(order.vendor_line_items(vendor).count).to eq(3) }
+ describe '#can_vendor_cancel?' do
+ context 'when order is not completed' do
+ let(:order) { create(:order, state: 'splitted') }
+
+ it 'returns false' do
+ expect(order.can_vendor_cancel?).to be false
+ end
end
- describe '#vendor_shipments' do
- it { expect(order.vendor_shipments(vendor).to_a).to eq([shipment]) }
- it { expect(order.vendor_shipments(vendor).count).to eq(1) }
+ context 'when order is canceled' do
+ let(:order) { create(:order, state: 'canceled') }
+
+ it 'returns false' do
+ expect(order.can_vendor_cancel?).to be false
+ end
end
- describe '#vendor_subtotal' do
- it { expect(order.vendor_subtotal(vendor)).to eq(320) }
+ context 'when vendor is not present' do
+ let!(:order) { create(:vendor_order, vendor: nil) }
+
+ it 'returns false' do
+ expect(order.can_vendor_cancel?).to be false
+ end
end
- describe '#display_subtotal' do
- it { expect(order.display_vendor_subtotal(vendor).to_s).to eq('$320.00') }
+ context 'when order wasn\'t created in external store' do
+ let(:order) { create(:vendor_completed_order_with_totals, vendor: create(:approved_vendor)) }
+
+ it 'returns true' do
+ expect(order.can_cancel_in_admin?).to be true
+ end
end
+ end
- describe '#vendor_pre_tax_item_amount' do
- it { expect(order.vendor_pre_tax_item_amount(vendor)).to eq(300) }
+ describe '#can_cancel_in_admin?' do
+ context 'when order is not completed' do
+ let(:order) { create(:order, state: 'splitted') }
+
+ it 'returns false' do
+ expect(order.can_cancel_in_admin?).to be false
+ end
end
- describe '#displayvendor_pre_tax_item_amount' do
- it { expect(order.display_vendor_pre_tax_item_amount(vendor).to_s).to eq('$300.00') }
+ context 'when order is canceled' do
+ let(:order) { create(:order, state: 'canceled') }
+
+ it 'returns false' do
+ expect(order.can_cancel_in_admin?).to be false
+ end
end
- describe '#vendor_pre_tax_total' do
- it { expect(order.vendor_pre_tax_total(vendor)).to eq(320) }
+ context 'when order is parent order' do
+ let!(:order) { create(:vendor_completed_order_with_totals, vendor: create(:approved_vendor)) }
+
+ it 'returns false' do
+ expect(order.parent.can_cancel_in_admin?).to be false
+ end
end
- describe '#displayvendor_pre_tax_total' do
- it { expect(order.display_vendor_pre_tax_total(vendor).to_s).to eq('$320.00') }
+ context 'when order wasn\'t created in external store' do
+ let(:order) { create(:vendor_completed_order_with_totals, vendor: create(:approved_vendor)) }
+
+ it 'returns true' do
+ expect(order.can_cancel_in_admin?).to be true
+ end
end
- context 'ship total' do
+ context 'when order is already shipped' do
+ let(:order) do
+ create(:vendor_order_ready_to_ship, vendor: create(:approved_vendor))
+ end
+
before do
- order.vendor_shipments(vendor).update_all(adjustment_total: 5)
+ order.shipments.find_each(&:ship!)
end
- describe '#vendor_ship_total' do
- it { expect(order.vendor_ship_total(vendor)).to eq(25) }
+ it 'returns false' do
+ expect(order.can_cancel_in_admin?).to be false
end
+ end
+ end
- describe '#display_vendor_ship_total' do
- it { expect(order.display_vendor_ship_total(vendor).to_s).to eq('$25.00') }
+ context 'multi vendor order' do
+ let(:parent_order) { create(:completed_order_with_totals) }
+
+ let(:vendor) { create(:approved_vendor) }
+ let(:vendor_2) { create(:approved_vendor) }
+
+ let(:product) { create(:product_in_stock, vendor: vendor, price: 90.0) }
+ let(:product_2) { create(:product_in_stock, vendor: vendor, price: 49.0) }
+ let(:product_3) { create(:product_in_stock, vendor: vendor_2, price: 100.0) }
+
+ let!(:line_item) { create(:line_item, order: parent_order, product: product, price: product.price) }
+ let!(:line_item_2) { create(:line_item, order: parent_order, product: product_2, price: product_2.price) }
+ let!(:line_item_3) { create(:line_item, order: parent_order, product: product_3, price: product_3.price) }
+
+ let!(:payment) { create(:payment, order: parent_order) }
+
+ describe '#split!' do
+ it { expect { parent_order.split! }.to change { parent_order.state }.from('complete').to('splitted') }
+ end
+
+ it 'does not send regular confirmation email' do
+ expect(Spree::OrderMailer).not_to receive(:confirm_email)
+ parent_order.send(:deliver_order_confirmation_email)
+ end
+ end
+
+ describe '#deliver_splitted_order_confirmation_email' do
+ let(:parent_order) { create(:completed_order_with_totals) }
+
+ context 'when send_consumer_transactional_emails store setting is set to true' do
+ it 'sends splitted order confirmation email after reaching splitted state' do
+ expect(Spree::OrderMailer).to receive_message_chain(:splitted_order_confirm_email, :deliver_later).and_return true
+ parent_order.deliver_splitted_order_confirmation_email
+ end
+
+ it 'updates confirmation_delivered to true' do
+ parent_order.deliver_splitted_order_confirmation_email
+ expect(parent_order.reload.confirmation_delivered).to be true
+ end
+ end
+
+ context 'when send_consumer_transactional_emails store setting is set to false' do
+ before do
+ parent_order.store.preferred_send_consumer_transactional_emails = false
+ parent_order.store.save!
end
- describe '#vendor_pre_tax_ship_amount' do
- it { expect(order.vendor_pre_tax_ship_amount(vendor)).to eq(20) }
+ it 'does not send splitted order confirmation email after reaching splitted state' do
+ expect(Spree::OrderMailer).not_to receive(:splitted_order_confirm_email)
+ parent_order.deliver_splitted_order_confirmation_email
end
- describe '#display_vendor_pre_tax_ship_amount' do
- it { expect(order.display_vendor_pre_tax_ship_amount(vendor).to_s).to eq('$20.00') }
+ it 'does not update confirmation_delivered to true' do
+ parent_order.deliver_splitted_order_confirmation_email
+ expect(parent_order.reload.confirmation_delivered).to be false
end
end
+ end
+
+ describe '#deliver_vendor_order_notification_email' do
+ let(:vendor) { create(:vendor) }
+ let!(:vendor_user) { create(:vendor_user, vendor: vendor) }
+ let(:vendor_order) { create(:vendor_completed_order_with_totals, vendor: vendor, store: store) }
- describe '#vendor_promo_total' do
- it { expect(order.vendor_promo_total(vendor)).to eq(-30) }
+ context 'when deliver_vendor_order_notification_email? returns true' do
+ before do
+ allow(vendor_order).to receive(:deliver_vendor_order_notification_email?).and_return(true)
+ clear_enqueued_jobs
+ end
+
+ it 'sends splitted order confirmation email after reaching splitted state' do
+ expect do
+ vendor_order.deliver_vendor_order_notification_email && perform_enqueued_jobs(except: Spree::Addresses::GeocodeAddressJob)
+ end.to change { Spree::OrderMailer.deliveries.count }.by 1
+ end
+
+ it 'updates confirmation_delivered to true' do
+ vendor_order.deliver_vendor_order_notification_email
+ expect(vendor_order.reload.confirmation_delivered).to be true
+ end
end
- describe '#display_vendor_promo_total' do
- it { expect(order.display_vendor_promo_total(vendor).to_s).to eq('-$30.00') }
+ context 'when deliver_vendor_order_notification_email? returns false' do
+ before { allow(vendor_order).to receive(:deliver_vendor_order_notification_email?).and_return(false) }
+
+ it 'does not send splitted order confirmation email after reaching splitted state' do
+ perform_enqueued_jobs(except: Spree::Addresses::GeocodeAddressJob)
+ expect(Spree::OrderMailer).not_to receive(:vendor_order_confirm_email)
+ vendor_order.deliver_vendor_order_notification_email
+ end
+
+ it 'does not update confirmation_delivered to true' do
+ vendor_order.deliver_vendor_order_notification_email
+ expect(vendor_order.reload.confirmation_delivered).to be false
+ end
end
- context 'additional tax' do
+ context 'when in test mode' do
before do
- order.vendor_line_items(vendor).update_all(additional_tax_total: 10)
- order.vendor_shipments(vendor).update_all(additional_tax_total: 1)
+ allow(store).to receive(:preferred_test_mode).and_return(true)
end
- describe '#vendor_additional_tax_total' do
- it { expect(order.vendor_additional_tax_total(vendor)).to eq(31) }
+ it 'does not send vendor order notification email' do
+ expect(Spree::OrderMailer).not_to receive(:vendor_order_confirm_email)
+ vendor_order.deliver_vendor_order_notification_email
end
- describe '#display_vendor_additional_tax_total' do
- it { expect(order.display_vendor_additional_tax_total(vendor).to_s).to eq('$31.00') }
+ it 'does not update confirmation_delivered to true' do
+ perform_enqueued_jobs
+ vendor_order.deliver_vendor_order_notification_email
+ expect(vendor_order.reload.confirmation_delivered).to be false
end
end
+ end
- context 'included tax' do
- before do
- order.vendor_line_items(vendor).update_all(included_tax_total: 5)
- order.vendor_shipments(vendor).update_all(included_tax_total: 1)
+ describe '#deliver_vendor_order_notification_email?' do
+ context 'when order is not splitted' do
+ let(:order) { create(:order, parent: nil) }
+
+ it 'returns false' do
+ expect(order.deliver_vendor_order_notification_email?).to be_falsey
end
+ end
- describe '#vendor_included_tax_total' do
- it { expect(order.vendor_included_tax_total(vendor)).to eq(16) }
+ context 'when confirmation was delivered' do
+ let(:order) { create(:order, parent: create(:order), vendor: create(:vendor), confirmation_delivered: true) }
+
+ it 'returns false' do
+ expect(order.deliver_vendor_order_notification_email?).to be_falsey
end
+ end
- describe '#display_vendor_included_tax_total' do
- it { expect(order.display_vendor_included_tax_total(vendor).to_s).to eq('$16.00') }
+ context 'when order is splitted and confirmation has not been delivered' do
+ let(:order) { create(:order, parent: create(:order), vendor: create(:vendor)) }
+
+ it 'returns true' do
+ expect(order.deliver_vendor_order_notification_email?).to be_truthy
end
end
+ end
+
+ describe '#after_cancel' do
+ context 'with the gift card covering the whole payment' do
+ let(:order) { create(:vendor_completed_order_with_totals, parent: parent_order) }
+ let(:parent_order) { create(:splitted_order) }
+
+ let(:gift_card) { create(:gift_card, amount: 50) }
+
+ let!(:store_credit_payment_1) { create(:store_credit_payment, order: order, state: 'completed', amount: 40) }
+ let!(:store_credit_payment_2) { create(:store_credit_payment, order: order, state: 'invalid', amount: 40) }
- describe '#vendor_total' do
- it { expect(order.vendor_total(vendor)).to eq(290) }
+ before do
+ parent_order.update_column(:total, 50)
+ parent_order.apply_gift_card(gift_card)
+
+ order.update_column(:total, 40)
+ end
+
+ it 'voids the completed store credit payments' do
+ order.after_cancel
+
+ expect(store_credit_payment_1.reload.state).to eq('void')
+ expect(store_credit_payment_2.reload.state).to eq('invalid')
+ end
end
+ end
- describe '#display_vendor_total' do
- it { expect(order.display_vendor_total(vendor).to_s).to eq('$290.00') }
+ describe 'shipment state' do
+ let(:order) { create(:order_ready_to_ship) }
- context 'respects currency' do
- before { order.currency = 'GBP' }
+ context 'when there are multiple shipment states' do
+ before do
+ create_list(:shipment, 2, order: order, state: 'ready')
+ end
- it { expect(order.display_vendor_total(vendor).to_s).to eq('£290.00') }
+ context 'when one of the shipments is shipped' do
+ before do
+ order.shipments.reload.second.update(state: 'shipped')
+ end
+
+ it do
+ order.update_with_updater!
+ expect(order.reload.shipment_state).to eq('partial')
+ end
+ end
+
+ context 'when of the shipments is pending' do
+ before do
+ order.shipments.reload.second.update(state: 'pending')
+ end
+
+ it do
+ order.update_with_updater!
+ expect(order.reload.shipment_state).to eq('pending')
+ end
end
end
- describe '#vendor_item_count' do
- before { order.vendor_line_items(vendor).last.update(quantity: 2) }
+ context 'when all shipments have the same state' do
+ before do
+ create_list(:shipment, 2, order: order, state: 'shipped')
+ order.shipments.update_all(state: 'shipped')
+ end
- it { expect(order.vendor_item_count(vendor)).to eq(4) }
+ it do
+ order.update_with_updater!
+ expect(order.reload.shipment_state).to eq('shipped')
+ end
end
+ end
- describe '#vendor_ids' do
- it { expect(order.vendor_ids).to match_array([vendor.id, vendor_2.id]) }
+ describe '#total_after_fees' do
+ subject(:total_after_fees) { order.total_after_fees }
- context 'when a line item does not belong to any vendor' do
- before { order.vendor_line_items(vendor_2).each { |li| li.product.update_columns(vendor_id: nil) } }
- it { expect(order.vendor_ids).to match_array([vendor.id]) }
+ context 'for a parent order' do
+ let(:order) { create(:order, total: 100, platform_fee_total: 0) }
+
+ it 'returns the order total' do
+ expect(total_after_fees).to eq(100)
end
end
- describe '#vendor_totals' do
- it { expect(order.vendor_totals).to be_kind_of(Array) }
- it { expect(order.vendor_totals).not_to be_empty }
- it { expect(order.vendor_totals.first.order).to eq(order) }
- it { expect(order.vendor_totals.map(&:vendor)).to match_array([vendor, vendor_2]) }
+ context 'for a suborder' do
+ let(:order) { create(:vendor_order, total: 100, platform_fee_total: -10) }
+
+ it 'returns the order total excluding platform fee total' do
+ expect(total_after_fees).to eq(90)
+ end
end
end
end
diff --git a/spec/models/spree/payment_spec.rb b/spec/models/spree/payment_spec.rb
new file mode 100644
index 00000000..fa2c2e44
--- /dev/null
+++ b/spec/models/spree/payment_spec.rb
@@ -0,0 +1,75 @@
+describe Spree::Payment, type: :model do
+ context 'vendorized payment' do
+ it 'assigns vendor from order' do
+ order = create(:vendor_order_with_totals, total: 100)
+ payment = create(:payment, order: order)
+ expect(payment.vendor).to eq(order.vendor)
+ end
+ end
+
+ describe 'can_credit?' do
+ subject { payment.can_credit? }
+
+ let(:payment) { create(:payment, order: order, amount: 10) }
+
+ before do
+ allow(payment).to receive(:credit_allowed).and_return(credit_allowed)
+ end
+
+ context 'when the payment is not on a vendor order' do
+ let(:order) { create(:order, total: 100) }
+
+ context 'when credit allowed is greater than 0' do
+ let(:credit_allowed) { 10 }
+ it { is_expected.to be(true) }
+ end
+
+ context 'when credit allowed is 0' do
+ let(:credit_allowed) { 0 }
+ it { is_expected.to be(false) }
+ end
+
+ context 'when the payment is a store credit' do
+ let(:payment) { create(:store_credit_payment, order: order, amount: 10) }
+
+ context 'when the credit allowed is greater than 0' do
+ let(:credit_allowed) { 10 }
+ it { is_expected.to be(true) }
+ end
+
+ context 'when the credit allowed is 0' do
+ let(:credit_allowed) { 0 }
+ it { is_expected.to be(false) }
+ end
+ end
+ end
+
+ context 'when the payment is on a vendor order' do
+ let(:order) { create(:vendor_order, total: 100) }
+
+ context 'when credit allowed is greater than 0' do
+ let(:credit_allowed) { 10 }
+ it { is_expected.to be(true) }
+ end
+
+ context 'when credit allowed is 0' do
+ let(:credit_allowed) { 0 }
+ it { is_expected.to be(false) }
+ end
+
+ context 'when the payment is a store credit' do
+ let(:payment) { create(:store_credit_payment, order: order, amount: 10) }
+
+ context 'when the credit allowed is greater than 0' do
+ let(:credit_allowed) { 10 }
+ it { is_expected.to be(false) }
+ end
+
+ context 'when the credit allowed is 0' do
+ let(:credit_allowed) { 0 }
+ it { is_expected.to be(false) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb
index ac5eab79..85d9ab88 100644
--- a/spec/models/spree/product_spec.rb
+++ b/spec/models/spree/product_spec.rb
@@ -1,19 +1,104 @@
-require 'timecop'
+require 'spec_helper'
-describe Spree::Product do
+RSpec.describe Spree::Product, type: :model do
+ let(:store) { Spree::Store.default }
let(:vendor) { create(:vendor) }
- let(:product) { create(:product, vendor: vendor) }
- context '.whitelisted_ransackable_associations' do
- it { expect(described_class.whitelisted_ransackable_associations).to include('vendor') }
- it { expect(described_class.whitelisted_ransackable_associations.count).to be > 1 }
+ it 'has correct ransackable attributes' do
+ expect(described_class.whitelisted_ransackable_attributes).to include('vendor_id')
end
- it 'touches vendor after update' do
- time = Time.current + 1.hour
- product
- Timecop.freeze(time) do
- expect { product.touch }.to change { vendor.reload.updated_at }
+ context 'Callbacks' do
+ let(:product) { create(:product, stores: [store], vendor: vendor) }
+
+ describe '#auto_match_taxons' do
+ let(:product) { create(:product) }
+
+ context 'when product is created' do
+ let(:product) { build(:product) }
+
+ it 'calls #auto_match_taxons' do
+ expect(product).to receive(:auto_match_taxons).at_least(:once)
+ product.save!
+ end
+
+ context 'with automatic taxons' do
+ let!(:taxon) { create(:automatic_taxon, store: Spree::Store.default) }
+
+ it 'enqueues Spree::Products::AutoMatchTaxonsJob' do
+ expect { product.save! }.to have_enqueued_job(Spree::Products::AutoMatchTaxonsJob).exactly(:once)
+ end
+ end
+
+ context 'without automatic taxons' do
+ before do
+ product.stores.first.taxons.automatic.delete_all
+ end
+
+ it 'does not enqueue Spree::Products::AutoMatchTaxonsWorker' do
+ expect { product.save! }.to_not have_enqueued_job(Spree::Products::AutoMatchTaxonsJob)
+ end
+ end
+ end
+
+ context 'when product is updated' do
+ context 'when tags are updated' do
+ it 'calls #auto_match_taxons' do
+ expect(product.reload).to receive(:auto_match_taxons)
+ product.update!(tag_list: 'eco, vegan')
+ end
+ end
+
+ context 'when available_on is updated' do
+ it 'calls #auto_match_taxons' do
+ expect(product.reload).to receive(:auto_match_taxons)
+ product.update!(available_on: 2.days.from_now)
+ end
+ end
+
+ context 'when any other attribute is updated' do
+ it 'doesnt call #auto_match_taxons' do
+ expect(product.reload).not_to receive(:auto_match_taxons)
+ product.update!(name: 'new name')
+ end
+ end
+ end
+
+ context 'when product is touched' do
+ it 'doesnt call #auto_match_taxons' do
+ expect(product.reload).not_to receive(:auto_match_taxons)
+ product.touch
+ end
+ end
+ end
+
+ describe '#update_vendor_products_count' do
+ it 'updates vendor products count' do
+ product
+ expect { create(:product, stores: [store], vendor: vendor) }.to change { vendor.reload.products_count }.by(1)
+ expect { product.destroy }.to change { vendor.reload.products_count }.by(-1)
+ end
+
+ it 'updates vendor updated_at' do
+ product = build(:product, stores: [store], vendor: vendor)
+
+ expect { product.save! }.to change(vendor, :updated_at)
+ expect { product.destroy }.to change { vendor.reload.updated_at }
+ end
+ end
+ end
+
+ describe '#restore' do
+ let!(:product) { create :product_in_stock }
+
+ before do
+ product.destroy!
+ create(:product_in_stock, name: product.name, stores: product.stores, vendor: product.vendor)
+ end
+
+ it 'restores product' do
+ expect { product.restore }.to change { product.reload.deleted_at }.to(nil)
+ expect(product.slug).not_to include('deleted')
end
end
end
diff --git a/spec/models/spree/product_with_vendor_spec.rb b/spec/models/spree/product_with_vendor_spec.rb
new file mode 100644
index 00000000..382a0e98
--- /dev/null
+++ b/spec/models/spree/product_with_vendor_spec.rb
@@ -0,0 +1,14 @@
+require 'timecop'
+
+describe Spree::Product do
+ let(:vendor) { create(:vendor) }
+ let(:product) { create(:product, vendor: vendor) }
+
+ it 'does not touch vendor after update' do
+ time = Time.current + 1.hour
+ product
+ Timecop.freeze(time) do
+ expect { product.touch }.not_to change { vendor.reload.updated_at }
+ end
+ end
+end
diff --git a/spec/models/spree/shipment_spec.rb b/spec/models/spree/shipment_spec.rb
new file mode 100644
index 00000000..f016193f
--- /dev/null
+++ b/spec/models/spree/shipment_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Shipment do
+ let(:store) { Spree::Store.default }
+ let(:shipment) { create(:shipment, state: :ready, order: order, stock_location: stock_location) }
+ let(:stock_location) { create(:stock_location, vendor: vendor) }
+ let(:vendor) { create(:vendor) }
+ let(:order) do
+ create(
+ :vendor_order_ready_to_ship,
+ parent: parent_order,
+ store: store,
+ vendor: vendor,
+ total: payment_amount
+ )
+ end
+ let(:payment) do
+ create(
+ :payment,
+ state: :completed,
+ amount: payment_amount,
+ order: order,
+ payment_method: gateway
+ )
+ end
+ let!(:gateway) { create(:credit_card_payment_method, stores: [store]) }
+ let(:payment_amount) { 50.0 }
+ let(:parent_order) { create(:order, state: 'splitted', parent: nil, number: 'PARENT', completed_at: Time.current) }
+
+ before do
+ order.update(payment_total: 0)
+ payment
+ end
+
+ describe '#cancel' do
+ it 'allows shipped shipment to be canceled' do
+ shipment.ship!
+ expect(shipment.shipped?).to eq true
+
+ shipment.cancel!
+ expect(shipment.canceled?).to eq true
+ end
+ end
+
+ context 'vendor order shipment' do
+ let(:parent_order) { create(:order, shipment_state: 'ready', state: 'splitted', completed_at: Time.current) }
+ let!(:vendor_order) { create(:vendor_order, vendor: create(:vendor), parent: parent_order, shipment_state: 'ready') }
+ let!(:shipment) do
+ create(:shipment, state: 'ready', order: vendor_order, tracking: '123456789')
+ end
+
+ before do
+ order.shipments.delete_all
+ allow(vendor_order).to receive(:can_ship?).and_return(true)
+ end
+
+ it 'updates both vendor and parent order state' do
+ expect { shipment.ship! }.to change { vendor_order.reload.shipment_state }.from('ready').to('shipped').and change {
+ parent_order.reload.shipment_state
+ }.from('ready').to('shipped')
+ end
+
+ context 'multiple shipments with diferent statuses' do
+ let!(:vendor_order_2) { create(:vendor_order, vendor: create(:vendor), parent: parent_order, shipment_state: 'ready') }
+ let!(:shipment2) { create(:shipment, state: 'ready', order: vendor_order) }
+
+ it 'updates both vendor and parent order state' do
+ expect { shipment.ship! }.to change { parent_order.reload.shipment_state }.from('ready').to('partial')
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/shipment_with_vendor_spec.rb b/spec/models/spree/shipment_with_vendor_spec.rb
new file mode 100644
index 00000000..be4193f7
--- /dev/null
+++ b/spec/models/spree/shipment_with_vendor_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Spree::Shipment, type: :model do
+ context 'vendorized shipment' do
+ it 'assigns vendor from stock location' do
+ stock_location = create(:stock_location, vendor: create(:vendor))
+ shipment = create(:shipment, stock_location: stock_location)
+ expect(shipment.vendor).to eq(stock_location.vendor)
+ end
+ end
+end
diff --git a/spec/models/spree/stock/coordinator_spec.rb b/spec/models/spree/stock/coordinator_spec.rb
new file mode 100644
index 00000000..57fc00c0
--- /dev/null
+++ b/spec/models/spree/stock/coordinator_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Stock::Coordinator, type: :model do
+ let(:coordinator) { described_class.new(order) }
+
+ describe '#build_packages' do
+ subject(:packages) { coordinator.build_packages }
+
+ shared_examples_for 'building one package' do
+ let(:stock_location) { Spree::StockLocation.last }
+
+ let(:order_products) { order.products }
+ let(:order_stock_locations) { order_products.flat_map(&:stock_items).map(&:stock_location).uniq }
+
+ let(:package_stock_location) { packages.first.stock_location }
+ let(:package_products) { packages.first.contents.map(&:variant).map(&:product) }
+
+ it 'builds one package for the same stock location' do
+ expect(order_stock_locations).to contain_exactly(stock_location)
+ expect(packages.count).to eq(1)
+ expect(package_stock_location.vendor).to eq(vendor)
+ expect(package_products).to contain_exactly(*order_products)
+ end
+ end
+
+ context 'for a single vendor' do
+ let(:order) { create(:vendor_order_with_line_items, line_items_count: 2, vendor: vendor) }
+ let(:vendor) { create(:vendor) }
+
+ include_examples 'building one package'
+ end
+
+ context 'for multiple vendors' do
+ let(:order) { create(:order, line_items: vendor_line_items) }
+ let(:vendor_line_items) { create_list(:vendor_line_item, 2) }
+
+ let(:vendor_1_line_item) { vendor_line_items[0] }
+ let(:vendor_1) { vendor_line_items[0].vendor }
+
+ let(:vendor_2_line_item) { vendor_line_items[1] }
+ let(:vendor_2) { vendor_line_items[1].vendor }
+
+ let(:order_stock_locations) { order.products.flat_map(&:stock_items).map(&:stock_location).uniq }
+ let(:package_stock_locations) { packages.map(&:stock_location) }
+
+ it 'builds one package for the same stock location per vendor' do
+ expect(package_stock_locations).to contain_exactly(*order_stock_locations)
+
+ vendor_1_package = packages.find { |package| package.stock_location.vendor == vendor_1 }
+ vendor_1_package_products = vendor_1_package.contents.map(&:variant).map(&:product)
+ expect(vendor_1_package_products).to contain_exactly(vendor_1_line_item.product)
+
+ vendor_2_package = packages.find { |package| package.stock_location.vendor == vendor_2 }
+ vendor_2_package_products = vendor_2_package.contents.map(&:variant).map(&:product)
+ expect(vendor_2_package_products).to contain_exactly(vendor_2_line_item.product)
+ end
+ end
+
+ context 'without vendors' do
+ let!(:order) { create(:order_with_line_items, line_items_count: 2) }
+ let(:vendor) { nil }
+
+ include_examples 'building one package'
+ end
+ end
+end
diff --git a/spec/models/spree/stock/package_with_vendor_spec.rb b/spec/models/spree/stock/package_with_vendor_spec.rb
new file mode 100644
index 00000000..8ae31235
--- /dev/null
+++ b/spec/models/spree/stock/package_with_vendor_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Package, type: :model do
+ subject { Package.new(stock_location, contents) }
+
+ let(:vendor) { create(:approved_vendor) }
+ let(:product) { create(:product, vendor: vendor) }
+ let(:variant) { product.master }
+ let(:stock_location) { build(:stock_location, vendor: vendor) }
+ let(:order) { create(:order) }
+ let(:contents) { [] }
+
+ def build_inventory_unit
+ build(:inventory_unit, variant: variant, order: order)
+ end
+
+ # Contains regression test for #2804
+ it 'builds a list of shipping methods common to all categories for stock location vendor' do
+ category1 = create(:shipping_category)
+ category2 = create(:shipping_category)
+ method1 = create(:shipping_method, vendor: vendor)
+ method2 = create(:shipping_method, vendor: vendor)
+ method3 = create(:shipping_method, vendor: create(:vendor))
+ method4 = create(:shipping_method)
+ method5 = create(:shipping_method, vendor: vendor)
+ method1.shipping_categories = [category1, category2]
+ method2.shipping_categories = [category1]
+ method3.shipping_categories = [category1, category2]
+ method4.shipping_categories = [category1, category2]
+ method5.shipping_categories = [category1, category2]
+ variant1 = create(:product, shipping_category: category1, vendor: vendor).master
+ variant2 = create(:product, shipping_category: category2, vendor: vendor).master
+ contents = [ContentItem.new(build(:inventory_unit, variant_id: variant1.id)),
+ ContentItem.new(build(:inventory_unit, variant_id: variant1.id)),
+ ContentItem.new(build(:inventory_unit, variant_id: variant2.id))]
+
+ package = Package.new(stock_location, contents)
+ expect(package.shipping_methods.pluck(:id)).to contain_exactly(method1.id, method5.id)
+ end
+
+ context 'shipping vendor product from non-vendor stock location' do
+ let(:central_stock_location) { build(:stock_location, vendor: nil) }
+
+ it 'builds a list of shipping methods common to all categories' do
+ category1 = create(:shipping_category)
+ category2 = create(:shipping_category)
+ method1 = create(:shipping_method)
+ method2 = create(:shipping_method, vendor: vendor)
+ method1.shipping_categories = [category1, category2]
+ method2.shipping_categories = [category1, category2]
+ variant1 = create(:product, shipping_category: category1).master
+ contents = [ContentItem.new(build(:inventory_unit, variant_id: variant1.id)),
+ ContentItem.new(build(:inventory_unit, variant_id: variant1.id))]
+
+ package = Package.new(central_stock_location, contents)
+ expect(package.shipping_methods).to eq([method1])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb
index ee4205dc..48e0dbb6 100644
--- a/spec/models/spree/variant_spec.rb
+++ b/spec/models/spree/variant_spec.rb
@@ -1,104 +1,9 @@
require 'spec_helper'
describe Spree::Variant do
- let(:vendor) { create :vendor }
- let(:other_vendor) { create :vendor, name: 'Other vendor' }
- let(:product) { create(:product, vendor: vendor) }
- subject { build(:variant, product: product) }
-
- context 'after create' do
- it "propagate to stock items only for Vendor's Stock Locations" do
- expect { create(:variant, product: product) }.to change {
- Spree::StockItem.where(stock_location: vendor.stock_locations.first).count
- }
- end
-
- it "doesn't propagate to stock items for other vendors Stock Locations" do
- expect { create(:variant, product: product) }.to_not change {
- Spree::StockItem.where(stock_location: other_vendor.stock_locations.first).count
- }
+ describe 'disable_sku_validation?' do
+ it 'returns true' do
+ expect(Spree::Variant.new.disable_sku_validation?).to be_truthy
end
end
-
- describe '#assign_vendor' do
- context 'product vendorized, variant vendorized' do
- it 'copies from product' do
- expect(subject.attributes['vendor_id']).to be_nil
- subject.save!
- expect(subject.reload.attributes['vendor_id']).to eq(product.vendor_id)
- end
- end
-
- context 'product not vendorized, variant vendorized' do
- before do
- SpreeMultiVendor::Config[:vendorized_models] = ['variant']
- end
-
- after do
- SpreeMultiVendor::Config[:vendorized_models] = SpreeMultiVendor::Configuration::DEFAULT_VENDORIZED_MODELS
- end
-
- context 'no vendor' do
- let(:product) { create(:product) }
-
- it { expect { subject.save! }.not_to change { subject.vendor }.from(nil) }
- end
-
- context 'vendor assigned' do
- subject { build(:variant, product: product, vendor: vendor) }
-
- it { expect { subject.save! }.not_to change { subject.vendor }.from(vendor) }
- end
- end
-
- context 'product vendorized, variant not vendorized' do
- before do
- SpreeMultiVendor::Config[:vendorized_models] = ['product']
- end
-
- after do
- SpreeMultiVendor::Config[:vendorized_models] = SpreeMultiVendor::Configuration::DEFAULT_VENDORIZED_MODELS
- end
-
- it { expect { subject.save! }.not_to raise_error }
- end
- end
-
- describe '#vendor' do
- context 'product vendorized, variant vendorized' do
- before { subject.save! }
-
- it { expect(subject.vendor).to eq(product.vendor) }
- end
-
- context 'product not vendorized, variant vendorized' do
- before do
- SpreeMultiVendor::Config[:vendorized_models] = ['variant']
- end
-
- after do
- SpreeMultiVendor::Config[:vendorized_models] = SpreeMultiVendor::Configuration::DEFAULT_VENDORIZED_MODELS
- end
-
- let(:product) { create(:product) }
- subject { build(:variant, product: product, vendor: vendor) }
-
- it { expect(subject.vendor).to eq(vendor) }
- end
-
- context 'product vendorized, variant not vendorized' do
- before do
- SpreeMultiVendor::Config[:vendorized_models] = ['product']
- subject.save!
- end
-
- after do
- SpreeMultiVendor::Config[:vendorized_models] = SpreeMultiVendor::Configuration::DEFAULT_VENDORIZED_MODELS
- end
-
- it { expect(subject.vendor).to eq(product.vendor) }
- end
- end
-
- it { expect(described_class.whitelisted_ransackable_associations).to include('vendor') }
end
diff --git a/spec/models/spree/vendor_order_totals_spec.rb b/spec/models/spree/vendor_order_totals_spec.rb
deleted file mode 100644
index f47d5d7c..00000000
--- a/spec/models/spree/vendor_order_totals_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'spec_helper'
-
-describe Spree::VendorOrderTotals do
- let(:vendor) { create(:vendor) }
- let(:order) { create(:completed_order_with_totals, line_items_count: 6, line_items_price: 100, shipment_cost: 50) }
- let!(:shipment) { create(:shipment, order: order, cost: 20, stock_location: vendor.stock_locations.first) }
- let!(:promotion) { create(:promotion_with_item_adjustment, adjustment_rate: 10, code: '10off') }
-
- subject { described_class.new(order: order, vendor: vendor) }
-
- before do
- order.products.update_all(vendor_id: vendor.id)
- order.vendor_line_items(vendor).update_all(additional_tax_total: 10)
- order.vendor_line_items(vendor).update_all(included_tax_total: 5)
- order.coupon_code = '10off'
- Spree::PromotionHandler::Coupon.new(order).apply
- order.reload
- end
-
- Spree::VendorOrderTotals::METHOD_NAMES.each do |method_name|
- describe "##{method_name}" do
- it { expect(subject.send(method_name)).to eq(order.send("vendor_#{method_name}", vendor)) }
- end
-
- describe "#display_#{method_name}" do
- it { expect(subject.send("display_#{method_name}")).to eq(order.send("display_vendor_#{method_name}", vendor)) }
- end
- end
-
- describe '#item_count' do
- it { expect(subject.item_count).to eq(order.vendor_item_count(vendor)) }
- end
-end
diff --git a/spec/models/spree/vendor_spec.rb b/spec/models/spree/vendor_spec.rb
index 019b92a5..0caf1175 100644
--- a/spec/models/spree/vendor_spec.rb
+++ b/spec/models/spree/vendor_spec.rb
@@ -1,26 +1,110 @@
require 'spec_helper'
describe Spree::Vendor do
- describe 'associations' do
- it { is_expected.to have_many(:commissions) }
- it { is_expected.to have_many(:products) }
- it { is_expected.to have_many(:shipping_methods) }
- it { is_expected.to have_many(:stock_locations) }
- it { is_expected.to have_many(:variants) }
- it { is_expected.to have_many(:vendor_users) }
- it { is_expected.to have_many(:users).through(:vendor_users) }
- it { is_expected.to have_one(:image) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to allow_value('flower').for(:name) }
- it { is_expected.to allow_value('flower455').for(:name) }
- it { is_expected.to allow_value('flower 455').for(:name) }
- it { is_expected.to allow_value('flower abc').for(:name) }
- it { is_expected.to allow_value('flower455 abc').for(:name) }
- it { is_expected.to allow_value('flower 455 abc').for(:name) }
- it { is_expected.to allow_value('flower & bucket').for(:name) }
+ let(:store) { Spree::Store.default }
+ let!(:store_admin) { create(:admin_user) }
+
+ describe 'Validations' do
+ let(:vendor) { build(:vendor) }
+
+ describe '#contact_person_cannot_be_store_admin' do
+ it 'is invalid' do
+ vendor.contact_person_email = store.users.first.email
+
+ expect(vendor).to_not be_valid
+ expect(vendor.errors.messages).to include(:contact_person_email)
+ expect(vendor.errors.full_messages).to include('Contact person email cannot belong to a store admin')
+ end
+
+ it 'is valid with a different email' do
+ expect(vendor).to be_valid
+ end
+ end
+ end
+
+ describe 'Callbacks' do
+ describe '#create_default_policies' do
+ let!(:vendor) { build(:vendor) }
+
+ it 'creates default policies' do
+ expect { vendor.save! }.to change(Spree::Policy, :count).by(SpreeMultiVendor::Config[:default_policies].count)
+ expect(vendor.policies.pluck(:name)).to contain_exactly(*SpreeMultiVendor::Config[:default_policies].map { |policy| Spree.t(policy) })
+ end
+ end
+
+ describe 'before_destroy :ensure_can_be_deleted' do
+ subject { vendor.destroy }
+
+ let!(:vendor) { create(:approved_vendor) }
+
+ context 'for a vendor with unfulfilled orders' do
+ let!(:vendor_order) { create(:vendor_order_ready_to_ship, vendor: vendor) }
+
+ it 'responds with an error' do
+ subject
+
+ expect(vendor.reload).to_not be_deleted
+ expect(vendor.errors.messages).to eq(base: ["Can't delete a partner with unfulfilled orders"])
+ end
+ end
+
+ context 'for a vendor with fulfilled orders' do
+ let!(:vendor_order) { create(:vendor_shipped_order, vendor: vendor) }
+
+ it 'soft-deletes the vendor' do
+ subject
+ expect(vendor.reload).to be_deleted
+ end
+ end
+ end
+
+ describe 'after_create' do
+ let!(:vendor) { build(:vendor) }
+
+ it 'creates a stock location with default country if no integration is providen' do
+ expect { vendor.save! }.to change(Spree::StockLocation, :count).by(1)
+ stock_location = Spree::StockLocation.last
+ expect(vendor.stock_locations.first).to eq stock_location
+ expect(stock_location.country).to eq Spree::Store.default.default_country
+ end
+ end
+
+ describe 'after_update' do
+ let!(:vendor) { create(:vendor) }
+ let!(:product_1) { create(:product, vendor: vendor) }
+ let!(:product_2) { create(:product, vendor: vendor) }
+ let!(:inactive_product) { create(:product, vendor: vendor, status: :draft) }
+
+ it 'updates stock_location names when vendor name changed' do
+ old_name = vendor.name
+ new_name = 'new vendor name'
+ vendor.name = new_name
+ expect(vendor.stock_locations.map(&:name).uniq).to eq [old_name]
+ vendor.save!
+ expect(vendor.stock_locations.map(&:name).uniq).to eq [new_name]
+ end
+ end
+ end
+
+ describe 'Translations' do
+ it 'has translatable about field' do
+ expect(described_class::TRANSLATABLE_FIELDS).to include(:about)
+ end
+
+ it 'supports translations for about' do
+ vendor = create(:vendor, about: 'About Vendor')
+
+ I18n.with_locale(:es) do
+ vendor.about = 'About Vendor in Spanish'
+ vendor.save!
+ end
+
+ expect(vendor.about.to_plain_text).to eq('About Vendor')
+
+ I18n.with_locale(:es) do
+ expect(vendor.about.to_plain_text).to eq('About Vendor in Spanish')
+ end
+ end
end
describe 'initial state' do
@@ -29,29 +113,191 @@
end
end
- describe 'after_create' do
- let!(:vendor) { build(:vendor) }
+ describe '#start_onboarding', job: true do
+ it 'should change state to onboarding' do
+ vendor = create(:vendor, state: 'pending')
+ vendor.start_onboarding
+ expect(vendor).to be_onboarding
+ vendor = create(:vendor, state: 'invited')
+ vendor.start_onboarding
+ expect(vendor).to be_onboarding
+ end
+ end
+
+ describe '#complete_onboarding', job: true do
+ context 'happy path' do
+ before do
+ allow_any_instance_of(Spree::Vendor).to receive(:accepted?).and_return(true)
+ allow_any_instance_of(Spree::Vendor).to receive(:onboarding_completed?).and_return(true)
+ end
+
+ it 'should change state to onboarding' do
+ vendor = create(:vendor, state: 'pending')
+ vendor.complete_onboarding
+ expect(vendor.state).to eq('ready_for_review')
+ vendor = create(:vendor, state: 'invited')
+ vendor.complete_onboarding
+ expect(vendor.state).to eq('ready_for_review')
+ vendor = create(:vendor, state: 'onboarding')
+ vendor.complete_onboarding
+ expect(vendor.state).to eq('ready_for_review')
+ end
+ end
+
+ context 'cannot complete onboarding' do
+ before do
+ allow_any_instance_of(Spree::Vendor).to receive(:onboarding_completed?).and_return(false)
+ end
- it 'creates a stock location with default country' do
- expect { vendor.save! }.to change(Spree::StockLocation, :count).by(1)
- stock_location = Spree::StockLocation.last
- expect(vendor.stock_locations.first).to eq stock_location
- expect(stock_location.country).to eq Spree::Store.default.default_country
+ it 'should not change state' do
+ vendor = create(:vendor, state: 'pending')
+ vendor.complete_onboarding
+ expect(vendor.state).to eq('pending')
+ vendor = create(:vendor, state: 'invited')
+ vendor.complete_onboarding
+ expect(vendor.state).to eq('invited')
+ vendor = create(:vendor, state: 'onboarding')
+ vendor.complete_onboarding
+ expect(vendor.state).to eq('onboarding')
+ end
end
+ end
+
+ context 'mails after state transitions' do
+ let!(:vendor) { create(:vendor, state: :pending) }
+ let(:mail_double) { double('Mail', deliver_later: true) }
+ let!(:marketplace_owner) { create(:admin_user) }
+
+ context 'after Vendor is suspended' do
+ before { vendor.approve }
+
+ it 'sends vendor_suspended emails to the vendor owners' do
+ expect(Spree::VendorMailer).to receive(:vendor_suspended).with(vendor).and_return(mail_double)
+ vendor.suspend
+ end
+ end
+
+ context 'after Vendor is approved' do
+ let!(:vendor_owner) { create(:vendor_user, vendor: vendor) }
+
+ before do
+ clear_enqueued_jobs
+ end
+
+ it 'sends vendor_approved emails to vendor owners and marketplace owners' do
+ expect { vendor.approve && perform_enqueued_jobs }.to change { Spree::VendorMailer.deliveries.count }.by (vendor.users.count + vendor.store.users.count)
+ expect(Spree::VendorMailer.deliveries.map(&:to).flatten).to match_array [vendor.users.map(&:email) + vendor.store.users.map(&:email)].flatten
+ end
+
+ context 'when there are many marketplace owners' do
+ let!(:additional_marketplace_owners) { create_list(:admin_user, 10) }
+
+ it 'sends one vendor_approved email per marketplace owner' do
+ expect { vendor.approve && perform_enqueued_jobs }.to change { Spree::VendorMailer.deliveries.count }.by (vendor.users.count + vendor.store.users.count)
+ end
+
+ it 'each email has only one recipient' do
+ vendor.approve && perform_enqueued_jobs
+ expect(Spree::VendorMailer.deliveries.map(&:to).map(&:length).uniq).to eq [1]
+ end
+ end
+ end
+
+ context 'after Vendor is rejected' do
+ it 'sends vendor_rejected emails to the vendor owners' do
+ expect(Spree::VendorMailer).to receive(:vendor_rejected).with(vendor).and_return(mail_double)
+ vendor.reject
+ end
+ end
+
+ context 'after Vendor starts onboarding' do
+ let!(:vendor) { create(:invited_vendor) }
+
+ it 'sends vendor_onboarding_started to marketplace owners' do
+ expect { vendor.start_onboarding && perform_enqueued_jobs }.to change(Spree::VendorMailer.deliveries, :count).by(store.users.count + 1)
+ expect(Spree::VendorMailer.deliveries.map(&:to).flatten).to contain_exactly(*store.users.pluck(:email), *vendor.contact_person_email)
+ end
+ end
+
+ context 'after vendor completes onboarding', job: true do
+ let!(:vendor) { create(:invited_vendor) }
+
+ before do
+ allow(vendor).to receive(:onboarding_completed?).and_return(true)
+ clear_enqueued_jobs
+ end
- it 'should act as list' do
- expect(vendor).to respond_to(:set_list_position)
+ it 'updates vendors state onboarding -> ready_for_review' do
+ expect { vendor.save! }.to change(vendor, :state).to('ready_for_review')
+ end
+
+ it 'sends vendor_onboarding_completed to store admins' do
+ expect { vendor.complete_onboarding! && perform_enqueued_jobs }.to change(Spree::VendorMailer.deliveries, :count).by(store.users.count)
+ expect(Spree::VendorMailer.deliveries.map(&:to).flatten).to contain_exactly(*store.users.pluck(:email))
+ end
+ end
+ end
+
+ context 'archiving products when suspended' do
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:products) { create_list(:product, 5, vendor: vendor) }
+ let!(:other_vendor_product) { create(:product, vendor_id: 'other_vendor_id', status: 'active') }
+
+ it 'archives all products of vendor in the background' do
+ expect { vendor.suspend }.to enqueue_job(SpreeMultiVendor::Vendors::ArchiveProductsJob).with(vendor.id)
+ end
+
+ it 'touches Taxons and Taxonomies' do
+ product = products.first
+ taxon_ids = product.taxons.map(&:self_and_ancestors).flatten.uniq.map(&:id)
+ taxons_updated_at = Spree::Taxon.where(id: taxon_ids).order(:created_at).pluck(:updated_at)
+ vendor.suspend
+
+ new_taxons_updated_at = Spree::Taxon.where(id: taxon_ids).order(:created_at).pluck(:updated_at)
+ taxons_updated_at.each_with_index do |old_updated_at, index|
+ expect(old_updated_at).to be_smaller_than(new_taxons_updated_at[index])
+ end
end
end
- describe 'after_update' do
- let!(:vendor) { create(:vendor) }
+ context 'initial platform_fee' do
+ let!(:vendor) { build(:vendor, platform_fee: platform_fee) }
+
+ context 'when platform fee is given' do
+ let(:platform_fee) { 17.00 }
+
+ it 'sets it to given value' do
+ vendor.save
+ expect(vendor.platform_fee).to eq 17.00
+ end
+ end
+
+ context 'when platform fee is not given' do
+ let(:platform_fee) { nil }
+
+ context 'when default store platform fee is set' do
+ before { vendor.store.update(preferred_platform_fee: 5.45) }
+
+ it 'sets it to default Store platform_fee' do
+ vendor.save
+ expect(vendor.platform_fee).to eq 5.45
+ end
+ end
+ end
+ end
+
+ describe '#default_stock_location' do
+ let!(:vendor) { build(:vendor) }
+ let(:store) { Spree::Store.default }
+
+ before { vendor.save! }
- it 'updates stock_location names when vendor name changed' do
- old_name = vendor.name
- new_name = 'new vendor name'
- vendor.name = new_name
- expect { vendor.save! }.to change { vendor.stock_locations.pluck(:name).uniq }.from([old_name]).to([new_name])
+ context 'default' do
+ it 'returns the vendor stock location' do
+ expect(vendor.default_stock_location).to eq(vendor.stock_locations.first)
+ expect(vendor.default_stock_location.name).to eq(vendor.name)
+ expect(vendor.default_stock_location.vendor).to eq(vendor)
+ end
end
end
end
diff --git a/spec/models/spree/vendor_user_spec.rb b/spec/models/spree/vendor_user_spec.rb
deleted file mode 100644
index 55f5de86..00000000
--- a/spec/models/spree/vendor_user_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'spec_helper'
-
-describe Spree::VendorUser do
- describe 'associations' do
- it { is_expected.to belong_to(:vendor) }
- it { is_expected.to belong_to(:user) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_uniqueness_of(:vendor_id).scoped_to(:user_id) }
- end
-end
diff --git a/spec/models/spree_multi_vendor/permission_sets/vendor_user_spec.rb b/spec/models/spree_multi_vendor/permission_sets/vendor_user_spec.rb
new file mode 100644
index 00000000..a69bcb11
--- /dev/null
+++ b/spec/models/spree_multi_vendor/permission_sets/vendor_user_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::PermissionSets::VendorUser, type: :model do
+ let(:vendor) { create(:approved_vendor) }
+ let(:vendor_user) { create(:vendor_user, vendor: vendor) }
+
+ let(:ability) { Spree::Ability.new(vendor_user) }
+
+ subject(:permission_set) { described_class.new(ability) }
+
+ before { permission_set.activate! }
+
+ describe '#activate!' do
+ context 'orders' do
+ let(:order) { create(:vendor_order, vendor: vendor) }
+
+ it 'cannot create' do
+ expect(ability).not_to be_able_to :create, order
+ end
+
+ it 'can admin' do
+ expect(ability).to be_able_to :admin, order
+ end
+
+ it 'can show' do
+ expect(ability).to be_able_to :show, order
+ end
+
+ it 'can index' do
+ expect(ability).to be_able_to :index, order
+ end
+
+ it 'can edit' do
+ expect(ability).to be_able_to :edit, order
+ end
+
+ it 'cannot update' do
+ expect(ability).to be_able_to :update, order
+ end
+
+ it 'can cart' do
+ expect(ability).to be_able_to :cart, order
+ end
+ end
+
+ context 'stock_locations' do
+ let(:other_vendor) { create(:approved_vendor) }
+ let(:stock_location) { create(:stock_location, vendor: vendor) }
+ let(:stock_location_other_vendor) { create(:stock_location, vendor: other_vendor) }
+
+ it 'can manage stock locations that are related to the vendor' do
+ expect(ability).to be_able_to(:manage, stock_location)
+ expect(ability).to_not be_able_to(:manage, stock_location_other_vendor)
+ end
+ end
+
+ context 'Spree::Taxon' do
+ let(:category) { create(:taxon) }
+
+ it 'can admin' do
+ expect(ability).to be_able_to(:admin, category)
+ end
+
+ it 'can select_options' do
+ expect(ability).to be_able_to(:select_options, category)
+ end
+ end
+
+ context 'Spree::PaymentMethod' do
+ let!(:store) { Spree::Store.default }
+ let!(:payment_method) { create(:payment_method, stores: [store]) }
+
+ it 'cannot create payment method' do
+ expect(ability).to_not be_able_to(:manage, payment_method)
+ end
+ end
+
+ context 'Spree::ShippingMethod' do
+ let!(:country_zone) { create(:zone, name: 'CountryZone') }
+ let!(:shipping_method) { create(:shipping_method, zones: [country_zone]) }
+
+ it 'can create shipping method' do
+ expect(ability).to be_able_to(:create, shipping_method)
+ end
+ end
+
+ context 'Spree::Product' do
+ let!(:store) { Spree::Store.default }
+ let!(:product) { create(:product, vendor: vendor, stores: [store]) }
+
+ it 'can create products' do
+ expect(ability).to be_able_to(:create, product)
+ end
+
+ it 'cannot manage commision' do
+ expect(ability).to_not be_able_to(:manage_commission, product)
+ end
+
+ it 'cannot activate product' do
+ expect(ability).to_not be_able_to(:activate, product)
+ end
+ end
+
+ context 'Spree::Address' do
+ let!(:address) { create(:address, user: create(:user)) }
+
+ it 'cannot create address' do
+ expect(ability).to_not be_able_to(:create, address)
+ end
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v1/products_spec.rb b/spec/requests/spree/api/v1/products_spec.rb
deleted file mode 100644
index 451ab9a6..00000000
--- a/spec/requests/spree/api/v1/products_spec.rb
+++ /dev/null
@@ -1,221 +0,0 @@
-require 'spec_helper'
-
-module Spree
- describe 'API V1 Products Spec', type: :request do
- let(:vendor) { create(:vendor, state: :active) }
- let(:other_vendor) { create(:vendor) }
- let(:shipping_category) { create(:shipping_category) }
- let(:selected_product) { product_without_vendor }
- let(:params) { {} }
-
- let!(:product_without_vendor) { create(:product) }
- let!(:product_with_vendor_vendor) { create(:product, vendor: vendor) }
- let!(:product_with_other_vendor) { create(:product, vendor: other_vendor) }
-
- before { stub_authentication! }
-
- describe 'products#index' do
- before { get '/api/v1/products', params: params }
-
- context 'as a regular user' do
- it 'gets a list of all products' do
- expect(response.status).to eq(200)
- expect(json_response[:products].size).to eq(3)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'gets a list of all products' do
- expect(response.status).to eq(200)
- expect(json_response[:products].size).to eq(3)
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- it 'gets a list of products assigned to that vendor' do
- expect(response.status).to eq(200)
- expect(json_response[:products].size).to eq(1)
- expect(json_response[:products][0][:id]).to eq(vendor.products.first.id)
- end
- end
- end
-
- describe 'products#show' do
- before { get "/api/v1/products/#{selected_product.id}" }
-
- it 'gets a product' do
- expect(response.status).to eq(200)
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- describe 'cannot view product of another vendor' do
- let(:selected_product) { product_with_other_vendor }
-
- it do
- expect(response.status).to eq(404)
- end
- end
-
- describe 'can view product assigned to that vendor' do
- let(:selected_product) { product_with_vendor_vendor }
-
- it do
- expect(response.status).to eq(200)
- end
- end
- end
- end
-
- describe 'products#create' do
- let(:params) do
- {
- product: {
- name: 'Product',
- shipping_category_id: shipping_category.id,
- price: 12.34
- }
- }
- end
-
- before { post '/api/v1/products', params: params }
-
- context 'as regular user' do
- it 'cannot create a new product' do
- expect(response.status).to eq(401)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can create a new product' do
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq(params[:product][:name])
- expect(json_response['price']).to eq(params[:product][:price].to_s)
- expect(json_response['shipping_category_id']).to eq(params[:product][:shipping_category_id])
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- it 'can create a new product assigned to vendor' do
- expect(response.status).to eq(201)
- product = Spree::Product.find(json_response[:id])
- expect(product.vendor).to eq(vendor)
- end
- end
- end
-
- describe 'products#update' do
- let(:params) do
- {
- product: {
- name: 'Updated Prodouct',
- shipping_category_id: shipping_category.id,
- price: 12.34
- }
- }
- end
-
- before { put "/api/v1/products/#{selected_product.id}", params: params }
-
- context 'as regular user' do
- it 'cannot update product' do
- expect(response.status).to eq(401)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can update product' do
- expect(response.status).to eq(200)
- expect(json_response['name']).to eq(params[:product][:name])
- expect(json_response['price']).to eq(params[:product][:price].to_s)
- expect(json_response['shipping_category_id']).to eq(params[:product][:shipping_category_id])
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- describe 'cannot update product of another vendor' do
- let(:selected_product) { product_with_other_vendor }
-
- it do
- expect(response.status).to eq(404)
- end
- end
-
- describe 'can update product assigned to that vendor' do
- let(:selected_product) { product_with_vendor_vendor }
-
- it do
- expect(response.status).to eq(200)
- end
- end
- end
- end
-
- describe 'products#destroy' do
- before { delete "/api/v1/products/#{selected_product.id}" }
-
- context 'as regular user' do
- it 'cannot delete product' do
- expect(response.status).to eq(401)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can delete product' do
- expect(response.status).to eq(204)
- expect(product_without_vendor.reload.deleted_at).to_not eq(nil)
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- describe 'cannot delete product of another vendor' do
- let(:selected_product) { product_with_other_vendor }
-
- it do
- expect(response.status).to eq(404)
- end
- end
-
- describe 'can delete product assigned to that vendor' do
- let(:selected_product) { product_with_vendor_vendor }
-
- it do
- expect(response.status).to eq(204)
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/spree/api/v1/users_spec.rb b/spec/requests/spree/api/v1/users_spec.rb
deleted file mode 100644
index ce54bc79..00000000
--- a/spec/requests/spree/api/v1/users_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require 'spec_helper'
-
-module Spree
- describe 'API V1 Users Spec', type: :request do
- let(:user) { create(:user) }
- let(:vendor) { create(:vendor, state: :active) }
-
- before { stub_authentication! }
-
- describe 'users#update' do
- let(:params) do
- {
- user: {
- vendor_ids: [vendor.id]
- }
- }
- end
-
- before { put "/api/v1/users/#{user.id}", params: params }
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can update user' do
- expect(response.status).to eq(200)
- expect(json_response['vendor_ids']).to eq([vendor.id])
- end
- end
- end
- end
-end
diff --git a/spec/requests/spree/api/v1/vendors_spec.rb b/spec/requests/spree/api/v1/vendors_spec.rb
deleted file mode 100644
index 84bdcae4..00000000
--- a/spec/requests/spree/api/v1/vendors_spec.rb
+++ /dev/null
@@ -1,210 +0,0 @@
-require 'spec_helper'
-
-module Spree
- describe 'API V1 Vendors Spec', type: :request do
- let(:params) { {} }
- let(:selected_vendor) { vendor }
-
- let!(:vendor) { create(:vendor, state: :active) }
- let!(:another_vendor) { create(:vendor, state: :active) }
-
- before { stub_authentication! }
-
- describe 'vendors#index' do
- before { get '/api/v1/vendors', params: params }
-
- context 'as a regular user' do
- it 'gets an empty list' do
- expect(response.status).to eq(200)
- expect(json_response.size).to eq(0)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'gets a list of all vendors' do
- expect(response.status).to eq(200)
- expect(json_response.size).to eq(2)
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- it 'gets a list of assigned vendors' do
- expect(response.status).to eq(200)
- expect(json_response.size).to eq(1)
- expect(json_response[0]['id']).to eq(vendor.id)
- end
- end
- end
-
- describe 'vendors#show' do
- before { get "/api/v1/vendors/#{selected_vendor.id}" }
-
- context 'as a regular user' do
- it 'cannot view a vendor' do
- expect(response.status).to eq(404)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
- it 'can view a vendor' do
- expect(response.status).to eq(200)
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- describe 'can view a assigned vendor' do
- it do
- expect(response.status).to eq(200)
- end
- end
-
- describe 'cannot view an another vendor' do
- let(:selected_vendor) { another_vendor }
-
- it do
- expect(response.status).to eq(404)
- end
- end
- end
- end
-
- describe 'vendors#create' do
- let(:params) do
- {
- vendor: {
- name: 'name'
- }
- }
- end
-
- before { post '/api/v1/vendors', params: params }
-
- context 'as regular user' do
- it 'cannot create a new vendor' do
- expect(response.status).to eq(401)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can create a new vendor' do
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq(params[:vendor][:name])
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor], email: FFaker::Internet.email)
- user
- end
-
- it 'cannot create a new vendor' do
- expect(response.status).to eq(401)
- end
- end
- end
-
- describe 'vendors#update' do
- let(:params) do
- {
- vendor: {
- name: 'updated name'
- }
- }
- end
-
- before { put "/api/v1/vendors/#{selected_vendor.id}", params: params }
-
- context 'as regular user' do
- it 'cannot update vendor' do
- expect(response.status).to eq(404)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can update vendor' do
- expect(response.status).to eq(200)
- expect(json_response['name']).to eq(params[:vendor][:name])
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- describe 'can update as assigned vendor' do
- it do
- expect(response.status).to eq(200)
- end
- end
-
- describe 'cannot update an another vendor' do
- let(:selected_vendor) { another_vendor }
-
- it do
- expect(response.status).to eq(404)
- end
- end
- end
- end
-
- describe 'vendors#destroy' do
- before { delete "/api/v1/vendors/#{selected_vendor.id}" }
-
- context 'as regular user' do
- it 'cannot delete vendor' do
- expect(response.status).to eq(404)
- end
- end
-
- context 'as an admin' do
- sign_in_as_admin!
-
- it 'can delete vendor' do
- expect(response.status).to eq(204)
- expect(vendor.reload.deleted_at).to_not eq(nil)
- end
- end
-
- context 'as a vendor' do
- let!(:current_api_user) do
- user = create(:user, vendors: [vendor])
- user
- end
-
- describe 'can delete an assigned vendor' do
- it do
- expect(response.status).to eq(204)
- end
- end
-
- describe 'cannot delete an another vendor' do
- let(:selected_vendor) { another_vendor }
-
- it do
- expect(response.status).to eq(404)
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/spree/api/v2/platform/orders_spec.rb b/spec/requests/spree/api/v2/platform/orders_spec.rb
new file mode 100644
index 00000000..c4b37659
--- /dev/null
+++ b/spec/requests/spree/api/v2/platform/orders_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe 'API V2 Platform Orders Spec' do
+ include_context 'Platform API v2'
+ let(:bearer_token) { { 'Authorization' => valid_authorization } }
+ let!(:order) { create(:vendor_order) }
+
+ describe 'get index' do
+ it 'should return only parent orders' do
+ get '/api/v2/platform/orders', headers: bearer_token
+
+ expect(response.status).to eq(200)
+ expect(json_response['data'].count).to eq(1)
+ expect(json_response['data'].first['id']).to eq(order.parent.id.to_s)
+ expect(json_response['data'].first['relationships']['vendor_orders']).to eq(
+ { "data" => [{ "id" => order.id.to_s, "type" => "order" }] }
+ )
+ end
+ end
+
+ describe 'get show' do
+ it 'should return order' do
+ get "/api/v2/platform/orders/#{order.id}", headers: bearer_token
+
+ expect(response.status).to eq(200)
+ expect(json_response['data']['id']).to eq(order.id.to_s)
+ expect(json_response['data']['relationships']['vendor_orders']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v2/platform/vendors_spec.rb b/spec/requests/spree/api/v2/platform/vendors_spec.rb
new file mode 100644
index 00000000..25fe5ed8
--- /dev/null
+++ b/spec/requests/spree/api/v2/platform/vendors_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+RSpec.describe 'API V2 Platform Vendors Spec' do
+ include_context 'Platform API v2'
+ let(:bearer_token) { { 'Authorization' => valid_authorization } }
+ let!(:vendor) { create(:vendor) }
+
+ describe 'GET show' do
+ it 'should return vendor' do
+ get "/api/v2/platform/vendors/#{vendor.id}", headers: bearer_token
+
+ expect(response.status).to eq(200)
+
+ expect(json_response['data']['id']).to eq(vendor.id.to_s)
+ expect(json_response['data']['attributes']['name']).to eq(vendor.name)
+ expect(json_response['data']['attributes']['about_us']).to eq(vendor.about.to_plain_text)
+
+ expect(json_response['data']['relationships']['metafields']).to eq('data' => [])
+ expect(json_response['data']['relationships']['policies']).to eq(
+ 'data' => [
+ {
+ 'id' => vendor.policies.first.id.to_s,
+ 'type' => 'policy'
+ }
+ ]
+ )
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v2/storefront/account/orders_spec.rb b/spec/requests/spree/api/v2/storefront/account/orders_spec.rb
new file mode 100644
index 00000000..9e74a5dc
--- /dev/null
+++ b/spec/requests/spree/api/v2/storefront/account/orders_spec.rb
@@ -0,0 +1,124 @@
+require 'spec_helper'
+
+RSpec.describe 'Storefront API v2 Orders spec', type: :request do
+ let(:store) { Spree::Store.default }
+ let!(:user) { create(:user_with_addresses) }
+ let!(:order) { create(:order, state: 'complete', user: user, completed_at: Time.current, store: store) }
+
+ before do
+ allow(Spree::Api::Config).to receive(:[]).and_call_original
+ allow(Spree::Api::Config).to receive(:[]).with(:api_v2_per_page_limit).and_return(2)
+ end
+
+ include_context 'API v2 tokens'
+
+ def expect_order_json(json)
+ expect(json).to be_present
+ expect(json).to have_id(order.id.to_s)
+ expect(json).to have_type('order')
+ expect(json).to have_attribute(:number).with_value(order.number)
+ expect(json).to have_attribute(:state).with_value(order.state)
+ expect(json).to have_attribute(:token).with_value(order.token)
+ expect(json).to have_attribute(:total).with_value(order.total.to_s)
+ expect(json).to have_attribute(:item_total).with_value(order.item_total.to_s)
+ expect(json).to have_attribute(:ship_total).with_value(order.ship_total.to_s)
+ expect(json).to have_attribute(:adjustment_total).with_value(order.adjustment_total.to_s)
+ expect(json).to have_attribute(:included_tax_total).with_value(order.included_tax_total.to_s)
+ expect(json).to have_attribute(:additional_tax_total).with_value(order.additional_tax_total.to_s)
+ expect(json).to have_attribute(:display_additional_tax_total).with_value(order.display_additional_tax_total.to_s)
+ expect(json).to have_attribute(:display_included_tax_total).with_value(order.display_included_tax_total.to_s)
+ expect(json).to have_attribute(:tax_total).with_value(order.tax_total.to_s)
+ expect(json).to have_attribute(:currency).with_value(order.currency.to_s)
+ expect(json).to have_attribute(:email).with_value(order.email)
+ expect(json).to have_attribute(:display_item_total).with_value(order.display_item_total.to_s)
+ expect(json).to have_attribute(:display_ship_total).with_value(order.display_ship_total.to_s)
+ expect(json).to have_attribute(:display_adjustment_total).with_value(order.display_adjustment_total.to_s)
+ expect(json).to have_attribute(:display_tax_total).with_value(order.display_tax_total.to_s)
+ expect(json).to have_attribute(:item_count).with_value(order.item_count)
+ expect(json).to have_attribute(:special_instructions).with_value(order.special_instructions)
+ expect(json).to have_attribute(:promo_total).with_value(order.promo_total.to_s)
+ expect(json).to have_attribute(:display_promo_total).with_value(order.display_promo_total.to_s)
+ expect(json).to have_attribute(:display_total).with_value(order.display_total.to_s)
+ expect(json).to have_attribute(:public_metadata).with_value(order.public_metadata)
+ expect(json).to have_relationships(:user, :line_items, :variants, :billing_address, :shipping_address, :payments, :shipments, :promotions)
+ end
+
+ shared_examples 'returns included resource' do |resource|
+ it 'returns included resource' do
+ expect(json_response['included'].size).to eq 1
+ expect(json_response['included'][0]).to have_type(resource)
+ end
+ end
+
+ describe 'orders#index' do
+ context 'with option: include' do
+ let!(:vendor_order) { create(:vendor_order_with_line_items, user: user, store: store, parent: order) }
+
+ shared_examples 'returns orders' do
+ it 'returns orders' do
+ expect(json_response['data'].size).to eq 1
+ expect(json_response['data']).to be_kind_of(Array)
+
+ expect_order_json(json_response['data'].first)
+ end
+ end
+
+ before { get "/api/v2/storefront/account/orders?include=#{include}", headers: headers_bearer }
+
+ context 'including vendors' do
+ let(:include) { 'vendors' }
+
+ it_behaves_like 'returns orders'
+
+ it_behaves_like 'returns included resource', 'vendor'
+ end
+ end
+
+ context 'with splitted orders' do
+ let!(:order_2) { create(:order, state: 'splitted', user: user, completed_at: Time.current) }
+
+ before { get '/api/v2/storefront/account/orders', headers: headers_bearer }
+
+ it 'returns splitted orders' do
+ expect(json_response['data'].count).to eq 2
+ expect(json_response['data'].pluck(:id)).to include(order_2.id.to_s)
+ end
+ end
+
+ context 'with vendor orders' do
+ let(:order_2) { create(:vendor_order, state: 'complete', user: user, completed_at: Time.current) }
+
+ before { get '/api/v2/storefront/account/orders', headers: headers_bearer }
+
+ it 'does not return vendor orders' do
+ expect(json_response['data'].count).to eq 1
+ expect(json_response['data'].pluck(:id)).not_to include(order_2.id.to_s)
+ end
+ end
+ end
+
+ describe 'orders#show' do
+ let(:user) { create(:user_with_addresses) }
+ let(:order) { create(:order, state: 'complete', user: user, completed_at: Time.current, store: store) }
+
+ shared_examples 'returns valid order JSON' do
+ it 'returns valid order JSON' do
+ expect_order_json(json_response['data'])
+ end
+ end
+
+ context 'with option: include' do
+ let!(:vendor_order) { create(:vendor_order_with_line_items, user: user, store: store, parent: order) }
+
+ before { get "/api/v2/storefront/account/orders/#{order.number}?include=#{include}", headers: headers_bearer }
+
+ context 'including vendors' do
+ let(:include) { 'vendors' }
+
+ it_behaves_like 'returns valid order JSON'
+
+ it_behaves_like 'returns included resource', 'vendor'
+ end
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v2/storefront/cart_spec.rb b/spec/requests/spree/api/v2/storefront/cart_spec.rb
deleted file mode 100644
index 0e859c40..00000000
--- a/spec/requests/spree/api/v2/storefront/cart_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe 'API V2 Storefront Cart Spec', type: :request do
- let(:default_currency) { 'USD' }
- let(:store) { Spree::Store.default }
- let(:currency) { store.default_currency }
- let(:user) { create(:user) }
- let(:order) { create(:order, user: user, store: store, currency: currency) }
- let!(:vendor) { create(:vendor) }
- let!(:vendor_2) { create(:vendor) }
- let(:product) { create(:product_in_stock, vendor: vendor) }
- let(:product_2) { create(:product_in_stock, vendor: vendor_2) }
-
- include_context 'API v2 tokens'
-
- describe 'cart#show' do
- let(:params) { {} }
- let!(:line_item) { create(:line_item, order: order, product: product, variant: product.default_variant) }
-
- shared_examples 'showing the cart' do
- before do
- get '/api/v2/storefront/cart', headers: headers_bearer, params: params
- end
-
- it_behaves_like 'returns 200 HTTP status'
- it_behaves_like 'returns valid cart JSON'
- end
-
- context 'with one vendor' do
- let(:params) { { include: 'vendors,vendor_totals' } }
-
- it_behaves_like 'showing the cart'
-
- it 'includes vendor and vendor totals' do
- get '/api/v2/storefront/cart', headers: headers_bearer, params: params
-
- expect(json_response['data']).to have_relationships(:vendors, :vendor_totals)
- expect(json_response[:included][0]).to have_id(vendor.id.to_s)
- expect(json_response[:included][0]).to have_type('vendor')
- expect(json_response[:included][1]).to have_id(vendor.id.to_s)
- expect(json_response[:included][1]).to have_type('vendor_totals')
- end
- end
-
- context 'with many vendors' do
- let!(:line_item_2) { create(:line_item, order: order, product: product_2, variant: product_2.default_variant) }
- let(:params) { { include: 'vendors,vendor_totals' } }
-
- it_behaves_like 'showing the cart'
-
- it 'includes many vendors and vendor totals' do
- get '/api/v2/storefront/cart', headers: headers_bearer, params: params
-
- [vendor, vendor_2].each do |vendor|
- expect(json_response['included']).to include(have_type('vendor').and have_id(vendor.id.to_s))
- expect(json_response['included']).to include(have_type('vendor_totals').and have_id(vendor.id.to_s))
- expect(json_response['included']).to include(have_type('vendor').and have_attribute(:name).with_value(vendor.name))
- expect(json_response['included']).to include(have_type('vendor_totals').and have_attribute(:name).with_value(vendor.name))
- end
- end
- end
-
- context 'includes line item vendor data' do
- it_behaves_like 'showing the cart'
-
- it 'via variants' do
- get '/api/v2/storefront/cart', params: { include: 'variants.vendor' }, headers: headers_bearer
- expect(json_response[:included][0]).to have_id(vendor.id.to_s)
- expect(json_response[:included][0]).to have_type('vendor')
- end
-
- it 'via products' do
- get '/api/v2/storefront/cart', params: { include: 'variants.product.vendor' }, headers: headers_bearer
- expect(json_response[:included][0]).to have_id(vendor.id.to_s)
- expect(json_response[:included][0]).to have_type('vendor')
- end
-
- it 'via line_items' do
- get '/api/v2/storefront/cart', params: { include: 'line_items.vendor' }, headers: headers_bearer
- expect(json_response[:included][0]).to have_id(vendor.id.to_s)
- expect(json_response[:included][0]).to have_type('vendor')
- end
- end
- end
-end
diff --git a/spec/requests/spree/api/v2/storefront/cart_with_vendors_spec.rb b/spec/requests/spree/api/v2/storefront/cart_with_vendors_spec.rb
new file mode 100644
index 00000000..5658b8c9
--- /dev/null
+++ b/spec/requests/spree/api/v2/storefront/cart_with_vendors_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe 'API V2 Storefront Cart Spec', type: :request do
+ let!(:store) { Spree::Store.default }
+ let(:currency) { store.default_currency }
+ let(:user) { create(:user) }
+ let(:order) { create(:order, user: user, store: store, currency: currency) }
+ let!(:vendor) { create(:approved_vendor) }
+ let(:product) { create(:product_in_stock, stores: [store], vendor: vendor) }
+ let(:variant) { product.default_variant }
+
+ include_context 'API v2 tokens'
+
+ describe 'cart#add_item' do
+ let(:options) { {} }
+ let(:params) { { variant_id: variant.id, quantity: 5, options: options, include: 'variants,line_items.vendor' } }
+ let(:execute) { post '/api/v2/storefront/cart/add_item', params: params, headers: headers_order_token }
+
+ before { execute }
+
+ it_behaves_like 'returns 200 HTTP status'
+ it_behaves_like 'returns valid cart JSON'
+
+ it 'adds vendor item' do
+ order.reload
+
+ expect(order.line_items.count).to eq(1)
+ expect(order.line_items.last.variant).to eq(variant)
+ expect(order.line_items.last.vendor).to eq(vendor)
+ expect(order.line_items.last.quantity).to eq(5)
+ expect(json_response['included']).to include(have_type('variant').and(have_id(variant.id.to_s)))
+ expect(json_response['included']).to include(have_type('vendor').and(have_id(vendor.id.to_s)))
+ end
+ end
+
+ describe 'cart#show' do
+ let!(:line_item) { create(:line_item, order: order, variant: variant, quantity: 5) }
+
+ let(:params) { { include: 'line_items.vendor' } }
+
+ let(:execute) { get '/api/v2/storefront/cart', headers: headers_order_token, params: params }
+
+ before { execute }
+
+ it_behaves_like 'returns 200 HTTP status'
+
+ it 'returns valid cart JSON' do
+ expect(json_response['included']).to include(have_type('vendor').and(have_id(vendor.id.to_s)))
+ end
+
+ context 'including vendors directly' do
+ let(:params) { { include: 'vendors' } }
+
+ it 'returns valid cart JSON' do
+ expect(json_response['included']).to include(have_type('vendor').and(have_id(vendor.id.to_s)))
+ end
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v2/storefront/checkout_with_vendors_spec.rb b/spec/requests/spree/api/v2/storefront/checkout_with_vendors_spec.rb
new file mode 100644
index 00000000..12021e90
--- /dev/null
+++ b/spec/requests/spree/api/v2/storefront/checkout_with_vendors_spec.rb
@@ -0,0 +1,362 @@
+require 'spec_helper'
+
+describe 'API V2 Storefront Checkout Spec', type: :request do
+ let!(:store) { Spree::Store.default }
+ let!(:vendor) { create(:approved_vendor) }
+
+ let(:currency) { store.default_currency }
+ let(:user) { create(:user) }
+ let(:order) { create(:order, user: user, store: store, currency: currency) }
+ let(:payment) { create(:payment, amount: order.total, order: order) }
+ let(:shipment) { create(:shipment, order: order) }
+
+ let(:address) do
+ {
+ firstname: 'John',
+ lastname: 'Doe',
+ address1: '7735 Old Georgetown Road',
+ city: 'Bethesda',
+ phone: '3014445002',
+ zipcode: '20814',
+ state_id: state.id,
+ country_iso: country.iso
+ }
+ end
+
+ let(:payment_source_attributes) do
+ {
+ gateway_payment_profile_id: 'BGS-123',
+ gateway_customer_profile_id: 'BGS-123',
+ month: 1.month.from_now.month,
+ year: 1.month.from_now.year,
+ name: 'Spree Commerce',
+ last_digits: '1111'
+ }
+ end
+ let(:payment_params) do
+ {
+ order: {
+ payments_attributes: [
+ {
+ payment_method_id: payment_method.id
+ }
+ ]
+ },
+ payment_source: {
+ payment_method.id.to_s => payment_source_attributes
+ }
+ }
+ end
+
+ include_context 'API v2 tokens'
+
+ describe 'checkout#shipping_rates' do
+ let(:execute) { get '/api/v2/storefront/checkout/shipping_rates', headers: headers }
+
+ let(:country) { store.default_country }
+ let(:zone) { create(:zone) }
+ let(:shipping_method) { create(:shipping_method, vendor: vendor) }
+ let(:shipping_method_2) { create(:shipping_method, vendor: vendor) }
+ let(:address) { create(:address, country: country) }
+
+ let(:shipment) { order.shipments.first }
+ let(:shipping_rate) { shipment.selected_shipping_rate }
+ let(:shipping_rate_2) { shipment.shipping_rates.where(selected: false).first }
+
+ shared_examples 'returns a list of shipments with shipping rates' do
+ before do
+ order.shipping_address = address
+ order.save!
+ zone.countries << country
+ shipping_method.zones = [zone]
+ shipping_method_2.zones = [zone]
+ order.create_proposed_shipments
+ execute
+ order.reload
+ end
+
+ it_behaves_like 'returns 200 HTTP status'
+
+ it 'returns valid shipments JSON' do
+ expect(json_response['data']).not_to be_empty
+ expect(json_response['data'].size).to eq(order.shipments.count)
+ expect(json_response['data'][0]).to have_id(shipment.id.to_s)
+ expect(json_response['data'][0]).to have_type('shipment')
+ expect(json_response['data'][0]).to have_relationships(:shipping_rates)
+ expect(json_response['included']).to be_present
+ expect(json_response['included'].size).to eq(shipment.shipping_rates.count + 2)
+ [{ shipping_method: shipping_method, shipping_rate: shipping_rate },
+ { shipping_method: shipping_method_2, shipping_rate: shipping_rate_2 }].each do |shipping|
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:name).with_value(shipping[:shipping_method].name)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:shipping_method_id).with_value(shipping[:shipping_method].id.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_id(shipping[:shipping_rate].id.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:cost).with_value(shipping[:shipping_rate].cost.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:tax_amount).with_value(shipping[:shipping_rate].tax_amount.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:selected).with_value(shipping[:shipping_rate].selected)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:final_price).with_value(shipping[:shipping_rate].final_price.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:free).with_value(shipping[:shipping_rate].free?)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:display_final_price).with_value(shipping[:shipping_rate].display_final_price.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:display_cost).with_value(shipping[:shipping_rate].display_cost.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_attribute(:display_tax_amount).with_value(shipping[:shipping_rate].display_tax_amount.to_s)))
+ expect(json_response['included']).to include(have_type('shipping_rate').and(have_relationship(:shipping_method).with_data({ 'id' => shipping[:shipping_method].id.to_s, 'type' => 'shipping_method' })))
+ end
+ expect(json_response['included']).to include(have_type('stock_location').and(have_id(shipment.stock_location_id.to_s)))
+ expect(json_response['included']).to include(have_type('stock_location').and(have_attribute(:name).with_value(shipment.stock_location.name)))
+ end
+ end
+
+ context 'as a guest user' do
+ include_context 'creates vendor guest order with guest token'
+
+ it_behaves_like 'returns a list of shipments with shipping rates'
+ end
+
+ context 'as a signed in user' do
+ include_context 'vendor order with a physical line item'
+
+ it_behaves_like 'returns a list of shipments with shipping rates'
+ end
+ end
+
+ describe 'checkout#select_shipping_method' do
+ let(:headers) { headers_bearer }
+ let(:params) do
+ {
+ shipping_method_id: shipping_method_2.id
+ }
+ end
+ let(:zone) { create(:zone_with_country) }
+
+ let(:country) { create(:country) }
+ let(:order) { create(:order, store: store, user: user, ship_address: create(:address, user: user, country: country)) }
+ let!(:line_item) { create(:vendor_line_item, order: order, vendor: vendor) }
+ let(:shipping_category) { order.products.first.shipping_category }
+ let!(:shipping_method) do
+ create(:shipping_method, vendor: vendor, zones: [zone], shipping_categories: [shipping_category]) do |shipping_method|
+ shipping_method.calculator.preferred_amount = 10
+ shipping_method.calculator.save
+ end
+ end
+ let!(:shipping_method_2) do
+ create(:shipping_method, vendor: vendor, zones: [zone], shipping_categories: [shipping_category]) do |shipping_method|
+ shipping_method.calculator.preferred_amount = 15
+ shipping_method.calculator.save
+ end
+ end
+ let!(:shipping_method_3) do
+ create(:shipping_method, vendor: vendor, zones: [create(:zone)], shipping_categories: [shipping_category]) do |shipping_method|
+ shipping_method.calculator.preferred_amount = 5
+ shipping_method.calculator.save
+ end
+ end
+ let(:shipment) { order.shipments.first }
+ let(:selected_shipping_rate) { shipment.selected_shipping_rate }
+ let(:execute) { patch '/api/v2/storefront/checkout/select_shipping_method?include=shipments.vendor', headers: headers, params: params }
+
+ before do
+ zone.countries << country
+ # making sure our store is in the geo zone supported by shipping method
+ store.update(checkout_zone: zone)
+ # generate shipping rates
+ get '/api/v2/storefront/checkout/shipping_rates', headers: headers
+ end
+
+ context 'one shipment' do
+ context 'valid shipping method' do
+ before { execute }
+
+ it_behaves_like 'returns 200 HTTP status'
+
+ it 'sets selected shipping method for shipment' do
+ expect(json_response['included']).to include(have_type('shipment').and(have_id(shipment.id.to_s)))
+ expect(json_response['included']).to include(have_type('vendor').and(have_id(vendor.id.to_s)))
+ expect(json_response['included'][1]).to have_relationship(:selected_shipping_rate).with_data({ 'id' => selected_shipping_rate.id.to_s,
+ 'type' => 'shipping_rate' })
+ expect(selected_shipping_rate.shipping_method).to eq(shipping_method_2)
+ end
+ end
+
+ context 'missing shipping method' do
+ let(:params) do
+ {
+ shipping_method_id: shipping_method_3.id
+ }
+ end
+
+ before { execute }
+
+ it_behaves_like 'returns 422 HTTP status'
+ end
+ end
+ end
+
+ orders_requiring_delivery = ['vendor order with a physical line item', 'vendor order with a physical and digital line item'].freeze
+
+ orders_requiring_delivery.each do |physical_goods_context|
+ describe "full checkout flow (#{physical_goods_context})" do
+ let!(:country) { create(:country) }
+ let(:state) { create(:state, country: country) }
+ let!(:shipping_method) do
+ create(:shipping_method, vendor: vendor).tap do |shipping_method|
+ shipping_method.zones = [zone]
+ end
+ end
+ let!(:zone) { create(:zone) }
+ let!(:zone_member) { create(:zone_member, zone: zone, zoneable: country) }
+ let!(:payment_method) { create(:credit_card_payment_method, stores: [store]) }
+
+ let(:customer_params) do
+ {
+ order: {
+ email: 'new@customer.org',
+ bill_address_attributes: address,
+ ship_address_attributes: address
+ }
+ }
+ end
+
+ let(:shipment_params) do
+ {
+ order: {
+ shipments_attributes: [
+ { selected_shipping_rate_id: shipping_rate_id, id: shipment_id }
+ ]
+ }
+ }
+ end
+
+ let(:shipping_rate_id) do
+ json_response['data'].first['relationships']['shipping_rates']['data'].first['id']
+ end
+ let(:shipment_id) { json_response['data'].first['id'] }
+
+ shared_examples 'transitions through checkout from start to finish' do
+ before do
+ zone.countries << country
+ shipping_method.zones = [zone]
+ end
+
+ it 'completes checkout' do
+ # we need to set customer information (email, billing & shipping address)
+ patch '/api/v2/storefront/checkout', params: customer_params, headers: headers
+ expect(response.status).to eq(200)
+
+ # getting back shipping rates
+ get '/api/v2/storefront/checkout/shipping_rates', headers: headers
+ expect(response.status).to eq(200)
+
+ # selecting shipping method
+ patch '/api/v2/storefront/checkout', params: shipment_params, headers: headers
+ expect(response.status).to eq(200)
+
+ # getting back list of available payment methods
+ get '/api/v2/storefront/checkout/payment_methods', headers: headers
+ expect(response.status).to eq(200)
+ expect(json_response['data'].first['id']).to eq(payment_method.id.to_s)
+
+ # creating a CC for selected payment method
+ patch '/api/v2/storefront/checkout', params: payment_params, headers: headers
+ expect(response.status).to eq(200)
+
+ # complete the checkout
+ patch '/api/v2/storefront/checkout/complete', headers: headers
+ expect(response.status).to eq(200)
+ expect(order.reload.completed_at).not_to be_nil
+ expect(order.state).to eq('complete')
+ expect(order.payments.valid.first.payment_method).to eq(payment_method)
+ end
+ end
+
+ context 'as a guest user' do
+ include_context 'creates vendor guest order with guest token'
+
+ it_behaves_like 'transitions through checkout from start to finish'
+ end
+
+ context 'as a signed in user' do
+ include_context physical_goods_context
+
+ it_behaves_like 'transitions through checkout from start to finish'
+ end
+ end
+ end
+
+ describe 'full checkout flow digital' do
+ let!(:country) { create(:country) }
+ let(:state) { create(:state, country: country) }
+ let!(:shipping_method) do
+ create(:shipping_method, vendor: vendor).tap do |shipping_method|
+ shipping_method.zones = [zone]
+ end
+ end
+ let!(:zone) { create(:zone) }
+ let!(:zone_member) { create(:zone_member, zone: zone, zoneable: country) }
+ let!(:payment_method) { create(:credit_card_payment_method, stores: [store]) }
+
+ let(:customer_params) do
+ {
+ order: {
+ email: 'new@customer.org',
+ bill_address_attributes: address,
+ ship_address_attributes: address
+ }
+ }
+ end
+
+ let(:shipment_params) do
+ {
+ order: {
+ shipments_attributes: [
+ { selected_shipping_rate_id: shipping_rate_id, id: shipment_id }
+ ]
+ }
+ }
+ end
+
+ let(:shipping_rate_id) do
+ json_response['data'].first['relationships']['shipping_rates']['data'].first['id']
+ end
+ let(:shipment_id) { json_response['data'].first['id'] }
+
+ shared_examples 'transitions through checkout from start to finish' do
+ before do
+ zone.countries << country
+ shipping_method.zones = [zone]
+ end
+
+ it 'completes checkout skipping delivery stage' do
+ # we need to set customer information (email, billing & shipping address)
+ patch '/api/v2/storefront/checkout', params: customer_params, headers: headers
+ expect(response.status).to eq(200)
+
+ # getting back list of available payment methods
+ get '/api/v2/storefront/checkout/payment_methods', headers: headers
+ expect(response.status).to eq(200)
+ expect(json_response['data'].first['id']).to eq(payment_method.id.to_s)
+
+ # creating a CC for selected payment method
+ patch '/api/v2/storefront/checkout', params: payment_params, headers: headers
+ expect(response.status).to eq(200)
+
+ # complete the checkout
+ patch '/api/v2/storefront/checkout/complete', headers: headers
+ expect(response.status).to eq(200)
+ expect(order.reload.completed_at).not_to be_nil
+ expect(order.state).to eq('complete')
+ expect(order.payments.valid.first.payment_method).to eq(payment_method)
+ end
+ end
+
+ context 'as a guest user' do
+ include_context 'creates vendor guest order with guest token'
+
+ it_behaves_like 'transitions through checkout from start to finish'
+ end
+
+ context 'as a signed in user' do
+ include_context 'vendor order with a digital line item'
+
+ it_behaves_like 'transitions through checkout from start to finish'
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v2/storefront/products_spec.rb b/spec/requests/spree/api/v2/storefront/products_spec.rb
deleted file mode 100644
index 19652f80..00000000
--- a/spec/requests/spree/api/v2/storefront/products_spec.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'spec_helper'
-
-describe 'API V2 Storefront Products Spec', type: :request do
- let(:store) { Spree::Store.default }
- let!(:products) { create_list(:product, 5, stores: [store]) }
- let!(:vendor) { create(:vendor) }
- let!(:vendor_2) { create(:vendor) }
-
- before do
- vendors = Spree::Vendor.all.to_a
- products.each do |product|
- product.vendor = vendors.sample
- product.save!
- end
- end
-
- describe 'products#index' do
- context 'with no params' do
- before { get '/api/v2/storefront/products' }
-
- it_behaves_like 'returns 200 HTTP status'
-
- it 'returns all products' do
- expect(json_response['data'].count).to eq Spree::Product.available.count
- expect(json_response['data'].first).to have_type('product')
- end
- end
-
- context 'with specified vendor ids' do
- before { vendor.products << create(:product) }
- before { vendor_2.products << create(:product) }
- before { get "/api/v2/storefront/products?filter[vendor_ids]=#{vendor.id}&include=vendor" }
-
- it_behaves_like 'returns 200 HTTP status'
-
- it 'returns products with specified ids' do
- expect(json_response['data'].count).to eq Spree::Product.available.where(vendor_id: vendor.id).count
- expect(json_response['included'].first).to have_id(vendor.id.to_s)
- expect(json_response['included'].first).to have_attribute(:name).with_value(vendor.name)
- expect(json_response['included'].first).to have_attribute(:about_us).with_value(vendor.about_us)
- end
- end
-
- context 'with specified multiple filters' do
- before { get "/api/v2/storefront/products?filter[skus]=#{products.first.sku}&filter[vendor_ids]=#{products.first.vendor_id}&include=vendor" }
-
- it_behaves_like 'returns 200 HTTP status'
-
- it 'returns products with specified SKU and Vendor ID' do
- expect(json_response['data'].count).to eq 1
- expect(json_response['data'].first).to have_id(products.first.id.to_s)
- expect(json_response['included'].first).to have_id(products.first.vendor_id.to_s)
- expect(json_response['included'].first).to have_attribute(:name).with_value(products.first.vendor.name)
- expect(json_response['included'].first).to have_attribute(:about_us).with_value(products.first.vendor.about_us)
- end
- end
- end
-
- describe 'products#show' do
- let(:product) { products.first }
-
- context 'with existing product' do
- before { get "/api/v2/storefront/products/#{product.slug}?include=vendor" }
-
- it_behaves_like 'returns 200 HTTP status'
-
- it 'returns a valid JSON response' do
- expect(json_response['data']).to have_id(product.id.to_s)
-
- expect(json_response['data']).to have_type('product')
-
- expect(json_response['data']).to have_attribute(:name).with_value(product.name)
- expect(json_response['data']).to have_attribute(:description).with_value(product.description)
- expect(json_response['data']).to have_attribute(:price).with_value(product.price.to_s)
- expect(json_response['data']).to have_attribute(:currency).with_value(product.currency)
- expect(json_response['data']).to have_attribute(:display_price).with_value(product.display_price.to_s)
- expect(json_response['data']).to have_attribute(:available_on).with_value(product.available_on.as_json)
- expect(json_response['data']).to have_attribute(:slug).with_value(product.slug)
- expect(json_response['data']).to have_attribute(:meta_description).with_value(product.meta_description)
- expect(json_response['data']).to have_attribute(:meta_keywords).with_value(product.meta_keywords)
- expect(json_response['data']).to have_attribute(:updated_at).with_value(product.updated_at.as_json)
- expect(json_response['data']).to have_attribute(:purchasable).with_value(product.purchasable?)
- expect(json_response['data']).to have_attribute(:in_stock).with_value(product.in_stock?)
- expect(json_response['data']).to have_attribute(:backorderable).with_value(product.backorderable?)
-
- expect(json_response['data']).to have_relationships(
- :variants, :option_types, :product_properties, :default_variant, :vendor
- )
-
- expect(json_response['included'].first).to have_id(product.vendor_id.to_s)
- expect(json_response['included'].first).to have_attribute(:name).with_value(product.vendor.name)
- expect(json_response['included'].first).to have_attribute(:about_us).with_value(product.vendor.about_us)
- end
- end
- end
-end
diff --git a/spec/requests/spree/api/v2/storefront/products_with_vendors_spec.rb b/spec/requests/spree/api/v2/storefront/products_with_vendors_spec.rb
new file mode 100644
index 00000000..0750bd22
--- /dev/null
+++ b/spec/requests/spree/api/v2/storefront/products_with_vendors_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe 'API V2 Storefront Products Spec', type: :request do
+ let!(:store) { Spree::Store.default }
+ let!(:products) { create_list(:product, 5, stores: [store]) }
+ let(:taxonomy) { create(:taxonomy, store: store) }
+ let!(:taxon) { taxonomy.root }
+ let(:product_with_taxon) { create(:product, taxons: [taxon], stores: [store]) }
+ let(:product_with_name) { create(:product, name: 'Test Product', stores: [store]) }
+ let(:product_with_price) { create(:product, price: 13.44, stores: [store]) }
+ let!(:option_type) { create(:option_type) }
+ let!(:option_value) { create(:option_value, option_type: option_type) }
+ let(:product_with_option) { create(:product, option_types: [option_type], stores: [store]) }
+ let!(:variant) { create(:variant, product: product_with_option, option_values: [option_value]) }
+ let(:product) { create(:product, stores: [store]) }
+ let!(:deleted_product) { create(:product, deleted_at: Time.current - 1.day, stores: [store]) }
+ let!(:discontinued_product) { create(:product, discontinue_on: Time.current - 1.day, stores: [store]) }
+ let!(:not_available_product) { create(:product, available_on: nil, stores: [store]) }
+ let!(:in_stock_product) { create(:product_in_stock, stores: [store]) }
+ let!(:not_backorderable_product) { create(:product_in_stock, :without_backorder, stores: [store]) }
+ let!(:property) { create(:property) }
+ let!(:new_property) { create(:property) }
+ let!(:product_with_property) { create(:product, stores: [store]) }
+ let!(:product_property) { create(:product_property, property: new_property, product: product_with_property, value: 'Some Value') }
+ let!(:product_property2) { create(:product_property, property: property, product: product_with_property, value: 'Some Value 2') }
+ let(:product_with_vendor_a) { create(:product, vendor: create(:approved_vendor), stores: [store]) }
+ let(:product_with_vendor_b) { create(:product, vendor: create(:approved_vendor), stores: [store]) }
+
+ before { Spree::Api::Config[:api_v2_per_page_limit] = 4 }
+
+ describe 'products#index' do
+ context 'include vendors' do
+ before { product_with_vendor_a }
+
+ it 'returns products with vendors' do
+ get '/api/v2/storefront/products?include=vendor'
+
+ expect(response).to be_successful
+ expect(json_response['included'].count).to eq(1)
+ expect(json_response['included'].first['type']).to eq('vendor')
+ expect(json_response['included'].first['id']).to eq(product_with_vendor_a.vendor.id.to_s)
+ end
+ end
+
+ context 'with specified vendor_ids' do
+ before { get "/api/v2/storefront/products?filter[vendor_ids]=#{product_with_vendor_a.vendor_id},#{product_with_vendor_b.vendor_id}" }
+
+ it_behaves_like 'returns 200 HTTP status'
+
+ it 'returns products with specified vendor_ids' do
+ expect(json_response['data']).not_to be_nil
+ expect(json_response['data'].count).to eq(2)
+ expect(json_response['data'].map(&:first).map(&:last)).to contain_exactly(product_with_vendor_a.id.to_s, product_with_vendor_b.id.to_s)
+ end
+ end
+
+ context 'with multiple specified options' do
+ let!(:color) { Spree::OptionType.find_by(name: 'color') || create(:option_type, :color) }
+ let!(:green_color) { color.option_values.find_by(name: 'green') || create(:option_value, option_type: color, name: 'green') }
+ let!(:white_color) { color.option_values.find_by(name: 'white') || create(:option_value, option_type: color, name: 'white') }
+
+ let!(:size) { Spree::OptionType.find_by(name: 'size') || create(:option_type, :size) }
+ let!(:s_size) { size.option_values.find_by(name: 's') || create(:option_value, option_type: size, name: 's') }
+ let!(:m_size) { size.option_values.find_by(name: 'm') || create(:option_value, option_type: size, name: 'm') }
+
+ let(:product_1) { create(:product, option_types: [color, size], stores: [store]) }
+ let!(:variant_1) { create(:variant, product: product_1, option_values: [white_color, m_size]) }
+
+ let(:product_2) { create(:product, option_types: [color, size], stores: [store]) }
+ let!(:variant_2_1) { create(:variant, product: product_2, option_values: [green_color, s_size]) }
+ let!(:variant_2_2) { create(:variant, product: product_2, option_values: [white_color, s_size]) }
+
+ context 'for filters with products' do
+ let(:options_filter) do
+ [
+ "filter[options][#{color.name}]=#{white_color.name}",
+ "filter[options][#{size.name}]=#{m_size.name}"
+ ].join('&')
+ end
+
+ before { get "/api/v2/storefront/products?#{options_filter}&include=option_types,variants.option_values" }
+
+ it_behaves_like 'returns 200 HTTP status'
+
+ it 'returns products with specified options' do
+ expect(json_response['data']).to include(have_id(product_1.id.to_s))
+ expect(json_response['data']).not_to include(have_id(product_2.id.to_s))
+
+ expect(json_response['included']).to include(have_type('option_type').and(have_attribute(:name).with_value(color.name)))
+ expect(json_response['included']).to include(have_type('option_value').and(have_attribute(:name).with_value(white_color.name)))
+
+ expect(json_response['included']).to include(have_type('option_type').and(have_attribute(:name).with_value(size.name)))
+ expect(json_response['included']).to include(have_type('option_value').and(have_attribute(:name).with_value(m_size.name)))
+ end
+ end
+ end
+
+ context 'with specified multiple filters' do
+ before { get "/api/v2/storefront/products?filter[name]=#{product_with_name.name}&filter[price]=#{product_with_name.price.to_f - 0.02},#{product_with_name.price.to_f + 0.02}" }
+
+ it_behaves_like 'returns 200 HTTP status'
+
+ it 'returns products with specified name and price' do
+ expect(json_response['data'].count).to eq 1
+ expect(json_response['data'].first).to have_id(product_with_name.id.to_s)
+ end
+ end
+
+ # Regression test for SD-1439 ambiguous column name: count_on_hand
+ context 'with multiple params' do
+ before do
+ get '/api/v2/storefront/products?filter[backorderable]=true'\
+ '&filter[ids]=130'\
+ '&filter[in_stock]=true'\
+ '&filter[name]=rails'\
+ '&filter[options][tshirt-color]=Red'\
+ '&filter[price]=10,100'\
+ '&filter[properties][brand-name]=alpha'\
+ '&filter[purchasable]=true'\
+ '&filter[show_deleted]=true'\
+ '&filter[show_discontinued]=true'\
+ '&filter[skus]=SKU-123,SKU-345'\
+ "&filter[taxons]=#{SecureRandom.uuid},#{SecureRandom.uuid},#{SecureRandom.uuid},#{SecureRandom.uuid},#{SecureRandom.uuid}"\
+ "&filter[vendor_ids]=#{SecureRandom.uuid},#{SecureRandom.uuid},#{SecureRandom.uuid},#{SecureRandom.uuid}"\
+ '&include=default_variant,variants,option_types,product_properties,taxons,images,primary_variant,vendor'\
+ '&page=1'\
+ '&per_page=25'\
+ '&sort=-updated_at,price,-name,created_at,-available_on,sku'
+ end
+
+ it_behaves_like 'returns 200 HTTP status'
+ end
+ end
+end
diff --git a/spec/requests/spree/api/v2/storefront/vendors_spec.rb b/spec/requests/spree/api/v2/storefront/vendors_spec.rb
index 4dc6ff47..66e240ec 100644
--- a/spec/requests/spree/api/v2/storefront/vendors_spec.rb
+++ b/spec/requests/spree/api/v2/storefront/vendors_spec.rb
@@ -1,117 +1,79 @@
require 'spec_helper'
-describe 'API V2 Storefront Vendor Spec', type: :request do
-
- let!(:vendor_image) { create(:vendor_image) }
- let!(:vendor) { create(:active_vendor, name: 'vendor', image: vendor_image) }
- let!(:product) { create(:product, vendor: vendor) }
- let!(:vendors) {
- create_list(:active_vendor_list, 30)}
-
+RSpec.describe 'API V2 Storefront Vendor Spec', type: :request do
describe 'vendors#show' do
- context 'with invalid slug param' do
+ let!(:vendor) { create(:approved_vendor, :with_logo, :with_cover_photo, about: 'About section') }
+ let!(:product) { create(:product, vendor: vendor) }
+
+ context 'with invalid vendor id' do
before { get '/api/v2/storefront/vendors/vendor1' }
it_behaves_like 'returns 404 HTTP status'
end
- context 'with valid slug param' do
- before { get "/api/v2/storefront/vendors/#{vendor.slug}" }
+ context 'with valid vendor id' do
+ let(:response_data) { json_response['data'] }
- it_behaves_like 'returns 200 HTTP status'
+ before { get "/api/v2/storefront/vendors/#{vendor.id}" }
- it 'returns one vendor' do
- json_response = JSON.parse(response.body)
- expect(json_response.count).to eq 1
- expect(json_response['data']['type']).to eq('vendor')
- end
+ it_behaves_like 'returns 200 HTTP status'
- it 'does not return included' do
- json_response = JSON.parse(response.body)
- expect(json_response.keys).to contain_exactly('data')
+ it 'returns vendor valid JSON response' do
+ expect(response_data['id']).to eq(vendor.id.to_s)
+ expect(response_data['type']).to eq('vendor')
+ expect(response_data['attributes']).to include(
+ 'name' => vendor.name,
+ 'email' => vendor.contact_person_email,
+ 'about_us' => 'About section'
+ )
+ expect(response_data['attributes']['logo_url']).to be_present
+ expect(response_data['attributes']['cover_photo_url']).to be_present
end
end
- context 'with products and images included' do
- before { get "/api/v2/storefront/vendors/#{vendor.slug}?include=products,image" }
+ context 'with vendor slug' do
+ before { get "/api/v2/storefront/vendors/#{vendor.slug}" }
it_behaves_like 'returns 200 HTTP status'
-
- it 'returns product information' do
- json_response = JSON.parse(response.body)
- expect(json_response.keys).to contain_exactly('data', 'included')
- expect(json_response['included'].first['id']).to eq(product.id.to_s)
- end
-
- it 'returns image information' do
- json_response = JSON.parse(response.body)
- expect(json_response.keys).to contain_exactly('data', 'included')
-
- expect(json_response['included'].second['id']).to eq(vendor_image.id.to_s)
- end
end
end
-
describe 'vendors#index' do
- context 'returns vendors list' do
-
- it 'must return a list of vendor paged' do
- get "/api/v2/storefront/vendors"
- expect(response).to have_http_status(:ok)
- json_response = JSON.parse(response.body)
- expect(json_response)
- expect(json_response['data'].count).to eq (25)
- end
-
- it 'can request different pages' do
- get "/api/v2/storefront/vendors?page=2"
- expect(response).to have_http_status(:ok)
- json_response = JSON.parse(response.body)
- expect(json_response)
- expect(json_response['data'].count).to eq (6)
-
- end
-
- it 'can control paging size' do
- get "/api/v2/storefront/vendors?page=2&per_page=10"
- expect(response).to have_http_status(:ok)
- json_response = JSON.parse(response.body)
- expect(json_response)
- expect(json_response['data'].count).to eq (10)
- end
+ let!(:vendor) { create(:approved_vendor, :with_logo, :with_cover_photo, about: 'About section') }
+ let!(:vendors) { create_list(:approved_vendor, 10) }
+ let(:params) { {} }
+ before { get '/api/v2/storefront/vendors', params: params }
- it 'returns data type vendor' do
- get "/api/v2/storefront/vendors"
- json_response = JSON.parse(response.body)
- expect(json_response)
- expect(json_response['data'][0]['type']).to eq('vendor')
- end
-
- it 'does not return included' do
- get "/api/v2/storefront/vendors"
- json_response = JSON.parse(response.body)
- expect(json_response.keys).to contain_exactly('data', "links", "meta")
- end
- end
-
- context 'with products and images included' do
- before { get "/api/v2/storefront/vendors?include=products,image" }
+ context 'returns vendors list' do
+ let(:vendor_data) { json_response['data'].find { |data| data['id'] == vendor.id.to_s } }
it_behaves_like 'returns 200 HTTP status'
- it 'returns product information' do
- json_response = JSON.parse(response.body)
- expect(json_response.keys).to contain_exactly('data', 'included', "links", "meta")
- expect(json_response['included'].first['id']).to eq(product.id.to_s)
+ it 'with all vendors' do
+ expect(json_response['data'].count).to eq (11)
end
- it 'returns image information' do
- json_response = JSON.parse(response.body)
- expect(json_response.keys).to contain_exactly('data', 'included',"links", "meta")
-
- expect(json_response['included'].second['id']).to eq(vendor_image.id.to_s)
+ it 'returns vendor valid JSON response' do
+ expect(vendor_data['id']).to eq(vendor.id.to_s)
+ expect(vendor_data['type']).to eq('vendor')
+ expect(vendor_data['attributes']).to include(
+ 'name' => vendor.name,
+ 'email' => vendor.contact_person_email,
+ 'about_us' => 'About section'
+ )
+ expect(vendor_data['attributes']['logo_url']).to be_present
+ expect(vendor_data['attributes']['cover_photo_url']).to be_present
+
+ expect(vendor_data['relationships']['metafields']).to eq('data' => [])
+ expect(vendor_data['relationships']['policies']).to eq(
+ 'data' => [
+ {
+ 'id' => vendor.policies.first.id.to_s,
+ 'type' => 'policy'
+ }
+ ]
+ )
end
end
end
diff --git a/spec/serializers/spree/api/v2/platform/vendor_serializer_spec.rb b/spec/serializers/spree/api/v2/platform/vendor_serializer_spec.rb
new file mode 100644
index 00000000..8a6c1dc1
--- /dev/null
+++ b/spec/serializers/spree/api/v2/platform/vendor_serializer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Spree::Api::V2::Platform::VendorSerializer do
+ include_context 'API v2 serializers params'
+
+ subject { described_class.new(vendor, params: serializer_params).serializable_hash }
+
+ let(:vendor) { create(:vendor) }
+
+ it 'returns the vendor name' do
+ expect(subject.dig(:data, :attributes, :name)).to eq(vendor.name)
+ end
+
+ it 'returns the slug' do
+ expect(subject.dig(:data, :attributes, :slug)).to eq(vendor.slug)
+ end
+
+ it 'returns the contact person email' do
+ expect(subject.dig(:data, :attributes, :contact_person_email)).to eq(vendor.contact_person_email)
+ end
+
+ it 'returns the public metadata' do
+ expect(subject.dig(:data, :attributes, :public_metadata)).to eq(vendor.public_metadata)
+ end
+end
\ No newline at end of file
diff --git a/spec/serializers/spree/v2/storefront/order_serializer_spec.rb b/spec/serializers/spree/v2/storefront/order_serializer_spec.rb
new file mode 100644
index 00000000..49d16dca
--- /dev/null
+++ b/spec/serializers/spree/v2/storefront/order_serializer_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe Spree::V2::Storefront::OrderSerializer do
+ include_context 'API v2 serializers params'
+
+ subject { described_class.new(order).serializable_hash }
+
+ let!(:parent_order) { create(:order, state: 'complete', completed_at: Time.current, store: store, parent: nil, vendor: nil) }
+ let!(:vendor_order) { create(:vendor_order_with_line_items, store: store, parent: parent_order) }
+
+ describe 'associations' do
+ describe '#line_items' do
+ context 'for parent order' do
+ let(:order) { parent_order }
+
+ it 'returns all line items' do
+ expect(subject[:data][:relationships][:line_items][:data].count).to eq 1
+ expect(subject[:data][:relationships][:line_items][:data][0][:id]).to eq vendor_order.line_items.take.id.to_s
+ end
+ end
+
+ context 'for vendor order' do
+ let(:order) { vendor_order }
+
+ it 'returns all line items' do
+ expect(subject[:data][:relationships][:line_items][:data].count).to eq 1
+ expect(subject[:data][:relationships][:line_items][:data][0][:id]).to eq vendor_order.line_items.take.id.to_s
+ end
+ end
+ end
+
+ describe '#variants' do
+ context 'for parent order' do
+ let(:order) { parent_order }
+
+ it 'returns all variants' do
+ expect(subject[:data][:relationships][:variants][:data].count).to eq 1
+ expect(subject[:data][:relationships][:variants][:data][0][:id]).to eq vendor_order.variants.take.id.to_s
+ end
+ end
+
+ context 'for vendor order' do
+ let(:order) { vendor_order }
+
+ it 'returns all variants' do
+ expect(subject[:data][:relationships][:variants][:data].count).to eq 1
+ expect(subject[:data][:relationships][:variants][:data][0][:id]).to eq vendor_order.variants.take.id.to_s
+ end
+ end
+ end
+
+ describe '#shipments' do
+ context 'for parent order' do
+ let(:order) { parent_order }
+
+ it 'returns all shipments' do
+ expect(subject[:data][:relationships][:shipments][:data].count).to eq 1
+ expect(subject[:data][:relationships][:shipments][:data][0][:id]).to eq vendor_order.shipments.take.id.to_s
+ end
+ end
+
+ context 'for vendor order' do
+ let(:order) { vendor_order }
+
+ it 'returns all shipments' do
+ expect(subject[:data][:relationships][:shipments][:data].count).to eq 1
+ expect(subject[:data][:relationships][:shipments][:data][0][:id]).to eq vendor_order.shipments.take.id.to_s
+ end
+ end
+ end
+
+ describe '#vendors' do
+ context 'for parent order' do
+ let(:order) { parent_order }
+
+ it 'returns all vendors' do
+ expect(subject[:data][:relationships][:vendors][:data].count).to eq 1
+ expect(subject[:data][:relationships][:vendors][:data][0][:id]).to eq vendor_order.vendors.take.id.to_s
+ end
+ end
+
+ context 'for vendor order' do
+ let(:order) { vendor_order }
+
+ it 'returns all vendors' do
+ expect(subject[:data][:relationships][:vendors][:data].count).to eq 1
+ expect(subject[:data][:relationships][:vendors][:data][0][:id]).to eq vendor_order.vendors.take.id.to_s
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/spree/v2/storefront/stock_location_serializer_spec.rb b/spec/serializers/spree/v2/storefront/stock_location_serializer_spec.rb
deleted file mode 100644
index 6528bc09..00000000
--- a/spec/serializers/spree/v2/storefront/stock_location_serializer_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Spree::V2::Storefront::StockLocationSerializer do
- let(:vendor) { create(:vendor, name: 'Test', about_us: 'Hello World') }
- let(:stock_location) { create(:stock_location, vendor: vendor) }
-
- subject { described_class.new(stock_location) }
-
- it { expect(subject.serializable_hash).to be_kind_of(Hash) }
- it do
- expect(subject.serializable_hash).to eq(
- {
- data: {
- id: stock_location.id.to_s,
- type: :stock_location,
- attributes: {
- name: stock_location.name
- },
- relationships: {
- vendor: {
- data: {
- id: vendor.id.to_s,
- type: :vendor
- }
- }
- }
- }
- }
- )
- end
-end
-
diff --git a/spec/serializers/spree/v2/storefront/taxon_serializer_spec.rb b/spec/serializers/spree/v2/storefront/taxon_serializer_spec.rb
new file mode 100644
index 00000000..9bef6102
--- /dev/null
+++ b/spec/serializers/spree/v2/storefront/taxon_serializer_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+RSpec.describe Spree::V2::Storefront::TaxonSerializer do
+ include_context 'API v2 serializers params'
+
+ subject { described_class.new(taxon.reload, params: serializer_params.merge({ include_products: include_products })).serializable_hash }
+
+ let(:taxon) { create(:taxon) }
+
+ let(:include_products) { false }
+
+ it { expect(subject).to be_kind_of(Hash) }
+
+ describe 'products' do
+ let(:include_products) { true }
+
+ let!(:active_product_from_approved_vendor) { create(:product, taxons: [taxon], vendor: create(:vendor)) }
+ let!(:active_product_from_unapproved_vendor) { create(:product, taxons: [taxon], vendor: create(:vendor, state: :onboarding)) }
+ let!(:inactive_product_from_approved_vendor) { create(:product, taxons: [taxon], vendor: create(:vendor), status: :draft) }
+ let!(:inactive_product_from_unapproved_vendor) do
+ create(:product, taxons: [taxon], vendor: create(:vendor, state: :onboarding), status: :draft)
+ end
+ let!(:active_product_without_vendor) { create(:product, taxons: [taxon], vendor: nil) }
+ let!(:inactive_product_without_vendor) { create(:product, taxons: [taxon], vendor: nil, status: :draft) }
+
+ it 'returns all products' do
+ expect(subject[:data][:relationships][:products][:data].count).to eq(6)
+ expect(subject[:data][:relationships][:products][:data]).to match_array(
+ [
+ { type: :product, id: active_product_from_approved_vendor.id.to_s },
+ { type: :product, id: active_product_from_unapproved_vendor.id.to_s },
+ { type: :product, id: inactive_product_from_approved_vendor.id.to_s },
+ { type: :product, id: inactive_product_from_unapproved_vendor.id.to_s },
+ { type: :product, id: active_product_without_vendor.id.to_s },
+ { type: :product, id: inactive_product_without_vendor.id.to_s }
+ ]
+ )
+ end
+ end
+
+ describe 'has_products' do
+ context 'when the taxon has products' do
+ let!(:product) { create(:product, taxons: [taxon]) }
+
+ it 'returns true' do
+ expect(subject[:data][:attributes][:has_products]).to be true
+ end
+ end
+
+ context 'when the taxon does not have products' do
+ it 'returns false' do
+ expect(subject[:data][:attributes][:has_products]).to be false
+ end
+ end
+ end
+end
diff --git a/spec/serializers/spree/v2/storefront/vendor_image_serializer_spec.rb b/spec/serializers/spree/v2/storefront/vendor_image_serializer_spec.rb
deleted file mode 100644
index 52257ae9..00000000
--- a/spec/serializers/spree/v2/storefront/vendor_image_serializer_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe Spree::V2::Storefront::VendorImageSerializer do
- let!(:vendor_image) { create(:vendor_image) }
- let!(:vendor) { create(:active_vendor, name: 'vendor', image: vendor_image) }
-
- subject { described_class.new(vendor_image) }
-
- it { expect(subject.serializable_hash).to be_kind_of(Hash) }
-
- it 'returns right data attributes' do
- expect(subject.serializable_hash[:data].keys).to contain_exactly(:id, :type, :attributes)
- end
-
- it 'returns right vendor_image attributes' do
- expect(subject.serializable_hash[:data][:attributes].keys).to contain_exactly(:styles)
- end
-end
diff --git a/spec/serializers/spree/v2/storefront/vendor_order_totals_serializer_spec.rb b/spec/serializers/spree/v2/storefront/vendor_order_totals_serializer_spec.rb
deleted file mode 100644
index 4860fdc1..00000000
--- a/spec/serializers/spree/v2/storefront/vendor_order_totals_serializer_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require 'spec_helper'
-
-describe Spree::V2::Storefront::VendorOrderTotalsSerializer do
- let(:vendor) { create(:vendor, name: 'Test', about_us: 'Hello World') }
- let(:order) { create(:completed_order_with_totals, line_items_count: 6, line_items_price: 100, shipment_cost: 50) }
- let!(:shipment) { create(:shipment, order: order, cost: 20, pre_tax_amount: 20, stock_location: vendor.stock_locations.first) }
- let!(:promotion) { create(:promotion_with_item_adjustment, adjustment_rate: 10, code: '10off') }
-
- let(:vendor_order_totals) { Spree::VendorOrderTotals.new(order: order, vendor: vendor) }
-
- before do
- order.products.update_all(vendor_id: vendor.id)
- order.vendor_line_items(vendor).update_all(additional_tax_total: 10)
- order.vendor_line_items(vendor).update_all(included_tax_total: 5)
- order.vendor_shipments(vendor).update_all(adjustment_total: 5)
- order.vendor_line_items(vendor).update_all(promo_total: 10)
- end
-
- subject { described_class.new(vendor_order_totals) }
-
- let(:expected_hash) do
- {
- data: {
- id: vendor.id.to_s,
- type: :vendor_totals,
- attributes: {
- name: 'Test',
- additional_tax_total: BigDecimal(60),
- display_additional_tax_total: Spree::Money.new(60),
- included_tax_total: BigDecimal(30),
- display_included_tax_total: Spree::Money.new(30),
- ship_total: BigDecimal(25),
- display_ship_total: Spree::Money.new(25),
- subtotal: BigDecimal(680),
- display_subtotal: Spree::Money.new(680),
- promo_total: BigDecimal(60),
- display_promo_total: Spree::Money.new(60),
- total: BigDecimal(625),
- display_total: Spree::Money.new(625),
- item_count: 6,
- pre_tax_item_amount: BigDecimal(600),
- display_pre_tax_item_amount: Spree::Money.new(600),
- pre_tax_ship_amount: BigDecimal(20),
- display_pre_tax_ship_amount: Spree::Money.new(20),
- pre_tax_total: BigDecimal(620),
- display_pre_tax_total: Spree::Money.new(620)
- }
- }
- }
- end
-
- it { expect(subject.serializable_hash).to be_kind_of(Hash) }
- it { expect(subject.serializable_hash).to eq(expected_hash) }
-end
diff --git a/spec/serializers/spree/v2/storefront/vendor_serializer_spec.rb b/spec/serializers/spree/v2/storefront/vendor_serializer_spec.rb
index 1f6bc4f7..8f935f23 100644
--- a/spec/serializers/spree/v2/storefront/vendor_serializer_spec.rb
+++ b/spec/serializers/spree/v2/storefront/vendor_serializer_spec.rb
@@ -1,34 +1,74 @@
require 'spec_helper'
describe Spree::V2::Storefront::VendorSerializer do
- let(:vendor) { create(:active_vendor, name: 'Test', about_us: 'Hello World', contact_us: 'Contact Info') }
-
- subject { described_class.new(vendor) }
-
- it { expect(subject.serializable_hash).to be_kind_of(Hash) }
-
- it do
- expect(subject.serializable_hash).to eq(
- {
- data: {
- id: vendor.id.to_s,
- type: :vendor,
- relationships: {
- image: {
- data: nil
- },
- products: {
- data: []
- }
- },
- attributes: {
- name: 'Test',
- contact_us: 'Contact Info',
- about_us: 'Hello World',
- slug: 'test'
- }
+ include_context 'API v2 serializers params'
+
+ subject { described_class.new(vendor, params: serializer_params.merge({ include_products: include_products })).serializable_hash }
+
+ let(:vendor) do
+ create(:vendor, about: "Click
here !")
+ end
+
+ let(:url_helpers) { Rails.application.routes.url_helpers }
+
+ let(:include_products) { false }
+
+ describe 'products' do
+ let(:include_products) { true }
+
+ let!(:active_product) { create(:product, vendor: vendor) }
+ let!(:inactive_product) { create(:product, vendor: vendor, status: :draft) }
+
+ it 'should return only products that are active' do
+ expect(subject[:data][:relationships][:products][:data].count).to eq(1)
+ expect(subject[:data][:relationships][:products][:data]).to match_array(
+ [
+ { type: :product, id: active_product.id.to_s },
+ ]
+ )
+ end
+ end
+
+ it 'returns the plain text of the about section' do
+ expect(subject[:data][:attributes][:about_us]).to eq("Click here!")
+ end
+
+ it 'returns the contact email' do
+ expect(subject.dig(:data, :attributes, :email)).to eq(vendor.contact_person_email)
+ end
+
+ it 'returns the slug' do
+ expect(subject.dig(:data, :attributes, :slug)).to eq(vendor.slug)
+ end
+
+ context 'with a logo' do
+ let(:vendor) { create(:vendor, :with_logo) }
+
+ it 'returns the logo url' do
+ expect(subject.dig(:data, :attributes, :logo_url)).to eq(url_helpers.cdn_image_url(vendor.logo.attachment))
+ end
+ end
+
+ context 'with a cover photo' do
+ let(:vendor) { create(:vendor, :with_cover_photo) }
+
+ it 'returns the cover photo url' do
+ expect(subject.dig(:data, :attributes, :cover_photo_url)).to eq(url_helpers.cdn_image_url(vendor.cover_photo.attachment))
+ end
+ end
+
+ context 'with metafields' do
+ before do
+ vendor.set_metafield('test.test', 'test')
+ end
+
+ it 'returns the metafields' do
+ expect(subject.dig(:data, :relationships, :metafields, :data)).to contain_exactly(
+ {
+ type: :metafield,
+ id: vendor.metafields.first.id.to_s
}
- }
- )
+ )
+ end
end
end
diff --git a/spec/services/spree/imports/row_processors/product_variant_spec.rb b/spec/services/spree/imports/row_processors/product_variant_spec.rb
new file mode 100644
index 00000000..a0117ead
--- /dev/null
+++ b/spec/services/spree/imports/row_processors/product_variant_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Imports::RowProcessors::ProductVariant, type: :service do
+ subject { described_class.new(row) }
+
+ let(:store) { Spree::Store.default }
+ let(:vendor) { create(:approved_vendor) }
+ let(:vendor_user) { create(:vendor_user, vendor: vendor) }
+ let(:import) { create(:product_import, owner: vendor, user: vendor_user) }
+ let(:row) { create(:import_row, import: import, data: row_data.to_json) }
+ let(:csv_row_headers) { Spree::ImportSchemas::Products.new.headers }
+ let(:variant) { subject.process! }
+
+ before do
+ import.create_mappings
+ end
+
+ # Matches how our production import will pass attributes
+ def csv_row_hash(attrs = {})
+ csv_row_headers.index_with { |header| attrs[header] }
+ end
+
+ let(:row_data) do
+ csv_row_hash(
+ 'slug' => 'denim-shirt',
+ 'name' => 'Denim Shirt',
+ 'status' => 'draft',
+ 'description' => 'Adipisci sapiente velit nihil ullam. Placeat cumque ipsa cupiditate velit magni sapiente mollitia dolorum. Veritatis esse illo eos perferendis. Perspiciatis vel iusto odio eveniet quam officia quidem. Fugiat a ipsum tempore optio accusantium autem in fugit.',
+ 'price' => '62.99',
+ 'currency' => 'USD',
+ 'weight' => '0.0',
+ 'inventory_count' => '100',
+ 'inventory_backorderable' => 'true',
+ 'tags' => 'ECO, Gold'
+ )
+ end
+
+ context 'when importing a master variant product row' do
+ it 'creates a product and associates it to the vendor' do
+ expect { subject.process! }.to change(Spree::Product, :count).by(1)
+ product = variant.product
+ expect(product).to be_persisted
+ expect(product.slug).to eq 'denim-shirt'
+ expect(product.name).to eq 'Denim Shirt'
+ expect(variant.vendor).to eq vendor
+ end
+ end
+
+ context 'fails when trying to update an existing non-vendor product' do
+ let!(:product) { create(:product, slug: 'denim-shirt', name: 'Denim Shirt', stores: [store]) }
+
+ it 'fails to update the product' do
+ expect { subject.process! }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+end
diff --git a/spec/services/spree/orders/create_vendor_order_spec.rb b/spec/services/spree/orders/create_vendor_order_spec.rb
new file mode 100644
index 00000000..7518ea99
--- /dev/null
+++ b/spec/services/spree/orders/create_vendor_order_spec.rb
@@ -0,0 +1,361 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Orders::CreateVendorOrder do
+ let(:store) { Spree::Store.default }
+ let!(:default_stock_location) { create(:stock_location, name: 'default', default: true) }
+ let(:user) { create(:user) }
+ let(:parent_order) do
+ create(:order, store: store, currency: 'USD', user: user,
+ special_instructions: 'Hi there', last_ip_address: '127.0.0.1',
+ bill_address: create(:address, user: user), ship_address: create(:address, user: user))
+ end
+ let(:vendor_order) { subject.value }
+
+ let(:vendor) { create(:approved_vendor, platform_fee: 10.to_d) }
+ let(:vendor_2) { create(:approved_vendor) }
+
+ let(:product) { create(:vendor_product_in_stock, vendor: vendor, price: 90.0) }
+ let(:product_2) { create(:vendor_product_in_stock, vendor: vendor, price: 49.0) }
+ let(:product_3) { create(:vendor_product_in_stock, vendor: vendor_2, price: 100.0) }
+ let(:variant) { product.default_variant }
+ let(:variant_2) { product_2.default_variant }
+ let(:variant_3) { product_3.default_variant }
+
+ let!(:line_item) { create(:line_item, order: parent_order, product: product, price: product.price) }
+ let!(:line_item_2) { create(:line_item, order: parent_order, product: product_2, price: product_2.price) }
+ let!(:line_item_3) { create(:line_item, order: parent_order, product: product_3, price: product_3.price) }
+
+ let!(:payment) { create(:payment, order: parent_order, amount: parent_order.total) }
+
+ subject { described_class.call(order: parent_order, vendor: vendor, index: 2) }
+
+ context 'dynamic shipping' do
+ let!(:shipment) { create(:shipment, order: parent_order, stock_location: vendor.stock_locations.first, cost: 5.0) }
+ let!(:shipment_2) { create(:shipment, order: parent_order, stock_location: vendor_2.stock_locations.first, cost: 5.0) }
+
+ let!(:shipment_adjustments) { create_list(:tax_adjustment, 2, adjustable: shipment, order: parent_order, amount: 10.0) }
+ let!(:shipment_2_adjustments) { create(:tax_adjustment, adjustable: shipment_2, order: parent_order) }
+
+ before do
+ parent_order.update_with_updater!
+ parent_order.update(
+ completed_at: '2020-11-01 10:00:00',
+ state: 'complete',
+ payment_state: 'balance_due',
+ shipment_state: 'pending',
+ confirmation_delivered: true
+ )
+ end
+
+ describe 'new vendor order' do
+ it { expect { subject }.to change { Spree::Order.has_vendor.count }.by(1) }
+
+ it 'creates a new vendor order' do
+ expect(vendor_order).to have_attributes(
+ number: "#{parent_order.number}-2",
+ email: user.email,
+ user_id: user.id,
+ store_id: store.id,
+ special_instructions: 'Hi there',
+ last_ip_address: '127.0.0.1',
+ state: 'complete',
+ payment_state: 'balance_due',
+ shipment_state: 'pending',
+ completed_at: parent_order.completed_at,
+ confirmation_delivered: true,
+ item_count: 2,
+ item_total: 139.to_d,
+ platform_fee_total: -13.9.to_d, # vendor fee is 10% of item total
+ shipment_total: 5.to_d,
+ adjustment_total: 0.to_d,
+ additional_tax_total: 1.to_d,
+ total: 144.to_d, # we don't include additional tax in vendor order total
+ vendor_id: vendor.id,
+ parent_id: parent_order.id,
+ currency: parent_order.currency
+ )
+ end
+
+ it 'calculates platform fee total correctly' do
+ expect(vendor_order.platform_fee_total).to eq(-0.1 * vendor_order.item_total)
+ end
+
+ it 'calculates total correctly' do
+ expect(vendor_order.total).to eq(
+ vendor_order.item_total + vendor_order.shipment_total + vendor_order.adjustment_total
+ )
+ end
+
+ it 'updates vendor cache' do
+ subject
+ expect(vendor.reload.sales_total).to eq vendor.orders.complete.sum(:total)
+ expect(vendor.commission_total).to eq vendor.orders.complete.sum(:platform_fee_total).abs
+ end
+
+ it 'cannot create the same order twice' do
+ expect { subject }.to change { Spree::Order.has_vendor.count }.by(1)
+ expect { subject }.not_to change { Spree::Order.has_vendor.count }
+ end
+
+ context 'if default store currency is different than parent order currency' do
+ it "sets vendor's order currency to parent_order's currency" do
+ allow_any_instance_of(Spree::Store).to receive(:default_currency).and_return('EUR')
+
+ subject
+ expect(vendor_order.currency).to eq 'USD'
+ end
+ end
+ end
+
+ describe 'moves vendor line items to vendor_order' do
+ it { expect { subject }.not_to change { Spree::LineItem.count } }
+ it { expect { subject }.not_to change { Spree::InventoryUnit.count } }
+ it { expect { subject }.not_to change { Spree::Adjustment.where.not(source: nil).count } }
+
+ it { expect { subject }.to change { parent_order.line_items.count }.from(3).to(1) }
+
+ it 'changes order for line items' do
+ expect(line_item.order).to eq(parent_order)
+ expect(line_item_2.order).to eq(parent_order)
+ expect(line_item_3.order).to eq(parent_order)
+ subject
+ expect(line_item.reload.order).to eq(vendor_order)
+ expect(line_item_2.reload.order).to eq(vendor_order)
+ expect(line_item_3.reload.order).to eq(parent_order)
+ end
+ end
+
+ describe 'assigns addresses to the new order' do
+ context 'guest order' do
+ before do
+ parent_order.update!(user: nil, email: 'new@email.com')
+ parent_order.bill_address.update!(user: nil)
+ parent_order.ship_address.update!(user: nil)
+ end
+
+ it 'assigns the same addresses' do
+ expect(vendor_order.bill_address).to eq(parent_order.bill_address.reload)
+ expect(vendor_order.ship_address).to eq(parent_order.ship_address.reload)
+ end
+ it { expect { subject }.not_to change(Spree::Address, :count) }
+ end
+
+ context 'signed in user' do
+ it 'assigns the same addresses' do
+ expect(vendor_order.bill_address).to eq(parent_order.bill_address.reload)
+ expect(vendor_order.ship_address).to eq(parent_order.ship_address.reload)
+ end
+ it { expect { subject }.not_to change(Spree::Address, :count) }
+ end
+ end
+
+ describe 'create_payments' do
+ it 'creates vendor payment' do
+ subject
+
+ vendor_payment = vendor_order.payments.last
+
+ expect(vendor_order.payments.count).to eq(1)
+ expect(vendor_payment.amount).to eq vendor_order.total
+ expect(vendor_payment.source).to eq payment.source
+ expect(vendor_payment.response_code).to eq payment.response_code
+ expect(vendor_payment.payment_method).to eq payment.payment_method
+ expect(vendor_payment.state).to eq 'completed'
+ end
+ end
+
+ describe 'create_store_credit_payments' do
+ let(:store_credit) { create(:store_credit, amount: store_credit_amount, user: parent_order.user) }
+ let(:store_credit_payment) { create(:store_credit_payment, source: store_credit, order: parent_order, amount: store_credit_amount, state: 'completed') }
+
+ context 'when order is partially covered by store credit' do
+ let(:store_credit_amount) { 20 }
+
+ before do
+ store_credit_payment
+ end
+
+ it 'creates a store credit payment with the proportional amount for a vendor order' do
+ subject
+
+ vendor_payment = vendor_order.payments.store_credits.last
+
+ expect(vendor_order.payments.store_credits.count).to eq(1)
+ expect(vendor_payment.vendor).to eq(vendor)
+ expect(vendor_payment.number).to eq("#{store_credit_payment.number}-2")
+ expect(vendor_payment.amount).to eq(11.63)
+ expect(vendor_payment.source).to eq(store_credit_payment.source)
+ expect(vendor_payment.response_code).to eq(store_credit_payment.response_code)
+ expect(vendor_payment.payment_method).to eq(store_credit_payment.payment_method)
+ expect(vendor_payment.state).to eq('completed')
+ end
+
+ it 'creates payment for the remaining amount' do
+ subject
+
+ cc_payment = vendor_order.payments.not_store_credits.last
+ store_credit_payment = vendor_order.payments.store_credits.last
+
+ expect(vendor_order.payments.not_store_credits.count).to eq(1)
+ expect(cc_payment.amount).to eq(vendor_order.item_total + vendor_order.ship_total - store_credit_payment.amount)
+ expect(cc_payment.state).to eq('completed')
+ end
+ end
+
+ context 'when order is fully covered by store credit' do
+ let(:store_credit_amount) { parent_order.total }
+
+ before do
+ payment.destroy
+ parent_order.reload
+
+ store_credit_payment
+ end
+
+ it 'creates a store credit payment using proportional total split' do
+ subject
+
+ vendor_payment = vendor_order.payments.store_credits.last
+
+ expect(vendor_order.payments.store_credits.count).to eq(1)
+ expect(vendor_payment.number).to eq("#{store_credit_payment.number}-2")
+ expect(vendor_payment.amount).to eq(144)
+ expect(vendor_payment.source).to eq(store_credit_payment.source)
+ expect(vendor_payment.payment_method).to eq(store_credit_payment.payment_method)
+ expect(vendor_payment.state).to eq('completed')
+ end
+
+ it 'does not create other payments' do
+ subject
+ expect(vendor_order.payments.not_store_credits.count).to eq(0)
+ end
+ end
+ end
+
+ describe 'moves vendor shipments to vendor_order' do
+ it 'changes order for shipments and shipment adjustments' do
+ expect(parent_order.shipments.count).to eq(2)
+ expect(parent_order.shipment_adjustments.count).to eq(3)
+
+ subject
+
+ expect(vendor_order.reload.shipments.count).to eq(1)
+ expect(vendor_order.shipment_adjustments.count).to eq(2)
+
+ expect(parent_order.shipments.count).to eq(1)
+ expect(parent_order.shipment_adjustments.count).to eq(1)
+ end
+
+ it 'assigns addresse from order to the new shipment' do
+ subject
+
+ expect(vendor_order.reload.shipments.pluck(:address_id).uniq).to eq([vendor_order.ship_address_id])
+ end
+ end
+
+ describe 'create_marketplace_commissions' do
+ it 'triggers SpreeMultiVendor::Fees::CreateAllItemsFees' do
+ expect(SpreeMultiVendor::Fees::CreateAllItemsFees).to receive_message_chain(:new, :call).and_return([])
+
+ subject
+ end
+
+ context 'when order is fully covered by store credit' do
+ before do
+ payment.destroy
+ parent_order.reload
+
+ store_credit = create(:store_credit, amount: parent_order.total, user: parent_order.user)
+ create(:store_credit_payment, source: store_credit, order: parent_order, amount: parent_order.total, state: 'completed')
+ end
+
+ it 'does not create marketplace commissions' do
+ expect(SpreeMultiVendor::Fees::CreateAllItemsFees).not_to receive(:new)
+ subject
+ end
+
+ it 'vendor order has zero platform fee total' do
+ subject
+ expect(vendor_order.platform_fee_total).to eq(0)
+ end
+ end
+ end
+ end
+
+ xcontext 'with commission discount' do
+ let(:promotion) { create(:promotion, kind: :automatic) }
+ let(:vendor) { create(:approved_vendor, platform_fee: 50.0) }
+ let(:vendor_2) { create(:approved_vendor, platform_fee: 20.0) }
+ let(:vendor_2_order) { described_class.call(order: parent_order, vendor: vendor_2, index: 1).value }
+
+ before do
+ Spree::Promotion::Actions::CreateAdjustment.create!(calculator: calculator, promotion: promotion)
+ promotion.activate(order: parent_order.reload)
+ end
+
+ context 'with amount calculator' do
+ let(:calculator) do
+ calculator = Spree::Calculator::FlatRate.new
+ calculator.preferred_amount = 50
+ calculator.preferred_currency = 'USD'
+ calculator
+ end
+
+ it 'should be counted as one usage' do
+ expect(promotion.reload.credits_count).to eq(1)
+ end
+
+ it 'calculates platform fee for vendor orders correctly' do
+ expect(parent_order.promo_total).to eq(-50)
+
+ expect(vendor_order.reload.platform_fee_total).to eq(-19.5)
+ expect(vendor_2_order.reload.platform_fee_total).to eq(-20)
+
+ expect(vendor_order.total.to_f).to eq(69.5) # 135 * 0.5
+ expect(vendor_2_order.total).to eq(80) # 100 * 0.8
+
+ expect(vendor_order.item_total).to eq(139)
+ expect(vendor_2_order.item_total).to eq(100)
+
+ expect(vendor_order.line_items.first.platform_fee_total).to eq(0.0)
+ expect(vendor_order.line_items.first.private_metadata[:platform_fee_discount].to_f).to eq(45)
+ expect(vendor_order.line_items.second.platform_fee_total).to eq(19.5)
+ expect(vendor_order.line_items.second.private_metadata[:platform_fee_discount].to_f).to eq(5.0)
+ expect(vendor_2_order.line_items.first.platform_fee_total).to eq(20)
+ expect(vendor_2_order.line_items.first.private_metadata[:platform_fee_discount].to_f).to eq(0.0)
+ end
+ end
+
+ xcontext 'with percent calculator' do
+ let(:calculator) do
+ calculator = Spree::Calculator::FlatPercentItemTotal.new
+ calculator.preferred_flat_percent = 10
+ calculator
+ end
+
+ it 'should be counted as one usage' do
+ expect(promotion.reload.credits_count).to eq(1)
+ end
+
+ it 'calculates platform fee for vendor orders correctly' do
+ expect(parent_order.promo_total).to eq(-23.9) # 10% of 239
+
+ expect(vendor_order.reload.platform_fee_total).to eq(-45.6)
+ expect(vendor_2_order.reload.platform_fee_total).to eq(-20)
+
+ expect(vendor_order.total.to_f).to eq(69.5)
+ expect(vendor_2_order.total).to eq(80)
+
+ expect(vendor_order.item_total).to eq(139)
+ expect(vendor_2_order.item_total).to eq(100)
+
+ expect(vendor_order.line_items.first.platform_fee_total).to eq(21.1)
+ expect(vendor_order.line_items.first.private_metadata[:platform_fee_discount].to_f).to eq(23.9)
+ expect(vendor_order.line_items.second.platform_fee_total).to eq(24.5)
+ expect(vendor_order.line_items.second.private_metadata[:platform_fee_discount].to_f).to eq(0.0)
+ expect(vendor_2_order.line_items.first.platform_fee_total).to eq(20)
+ expect(vendor_2_order.line_items.first.private_metadata[:platform_fee_discount].to_f).to eq(0.0)
+ end
+ end
+ end
+end
diff --git a/spec/services/spree/orders/split_by_vendor_spec.rb b/spec/services/spree/orders/split_by_vendor_spec.rb
new file mode 100644
index 00000000..2cf3c050
--- /dev/null
+++ b/spec/services/spree/orders/split_by_vendor_spec.rb
@@ -0,0 +1,205 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Orders::SplitByVendor do
+ let(:store) { Spree::Store.default }
+
+ let(:user) { create(:user) }
+ let(:parent_order) do
+ create(:completed_order_with_totals,
+ special_instructions: 'Hi there',
+ last_ip_address: '127.0.0.1',
+ without_line_items: true,
+ user: user,
+ bill_address: create(:address, user: user),
+ ship_address: create(:address, user: user))
+ end
+
+ let!(:vendor) { create(:approved_vendor) }
+ let!(:vendor_2) { create(:approved_vendor) }
+ let!(:vendor_user) { create(:vendor_user, vendor: vendor, email: vendor.contact_person_email) }
+ let!(:vendor_user_2) { create(:vendor_user, vendor: vendor_2, email: vendor_2.contact_person_email) }
+
+ let(:tax_category) { create(:tax_category) }
+ let(:tax_category_2) { create(:tax_category) }
+ let(:product) { create(:product_in_stock, tax_category: tax_category, vendor: vendor, price: 90.0) }
+ let(:product_2) { create(:product_in_stock, tax_category: tax_category, vendor: vendor, price: 49.0) }
+ let(:product_3) { create(:product_in_stock, tax_category: tax_category_2, vendor: vendor_2, price: 100.0) }
+
+ let(:variant) { product.default_variant }
+ let(:variant_2) { product_2.default_variant }
+ let(:variant_3) { product_3.default_variant }
+
+ let!(:line_item) { create(:line_item, order: parent_order, product: product, price: product.price) }
+ let!(:line_item_2) { create(:line_item, order: parent_order, product: product_2, price: product_2.price) }
+ let!(:line_item_3) { create(:line_item, order: parent_order, product: product_3, price: product_3.price) }
+
+ let(:order_item_total) { 239.to_d }
+ let(:order_promo_total) { -10.to_d }
+ let(:order_shipment_total) { 20.to_d }
+ let(:order_additional_tax_total) { 13.9.to_d }
+ let(:order_total) { order_item_total + order_promo_total + order_shipment_total + order_additional_tax_total }
+
+ let(:vendor_item_total) { 139.to_d }
+ let(:vendor_shipment_total) { 10.to_d }
+ let(:vendor_additional_tax_total) { 13.9.to_d }
+ let(:vendor_platform_fee_total) { -0.3 * vendor_item_total }
+ let(:vendor_order_total) { vendor_item_total + vendor_shipment_total }
+
+ let(:vendor_2_item_total) { 100.to_d }
+ let(:vendor_2_shipment_total) { 10.to_d }
+ let(:vendor_2_additional_tax_total) { 0.0.to_d }
+ let(:vendor_2_platform_fee_total) { -0.3 * vendor_2_item_total }
+ let(:vendor_2_order_total) { vendor_2_item_total + vendor_2_shipment_total }
+
+ before do
+ parent_order.shipments.destroy_all
+ end
+
+ context 'when not parent order' do
+ let(:vendor_order) { build(:order, parent: parent_order, vendor: vendor) }
+
+ it 'returns failed' do
+ expect(subject.call(order: vendor_order).success?).to be(false)
+ end
+
+ it 'does not send order confirmation email to vendor' do
+ expect { subject.call(order: vendor_order) }.not_to change(Spree::OrderMailer.deliveries, :count)
+ end
+ end
+
+ context 'when already all splitted' do
+ let!(:vendor_order) { create(:order, parent: parent_order, vendor: vendor) }
+ let!(:vendor_order_2) { create(:order, parent: parent_order, vendor: vendor_2) }
+
+ it 'returns failed' do
+ expect(subject.call(order: parent_order).success?).to be(false)
+ end
+
+ it 'does not send order confirmation email to vendor' do
+ expect { subject.call(order: vendor_order) }.not_to change(Spree::OrderMailer.deliveries, :count)
+ end
+ end
+
+ it 'calls the Spree::Orders::CreateVendorOrder service per vendor' do
+ create_vendor_order_service = double(:create_vendor_order_service, call: nil)
+ allow(Spree::Orders::CreateVendorOrder).to receive(:new).and_return(create_vendor_order_service)
+
+ subject.call(order: parent_order)
+
+ expect(create_vendor_order_service).to have_received(:call).twice
+ end
+
+ it 'splits the parent order into vendor_orders per supplier' do
+ create(:payment, order: parent_order)
+
+ subject.call(order: parent_order)
+
+ expect(parent_order.vendor_orders.count).to eq(2)
+ end
+
+ it 'sends vendor order confirmation email to each vendor' do
+ create(:payment, order: parent_order)
+ clear_enqueued_jobs
+ subject.call(order: parent_order)
+ perform_enqueued_jobs(except: Spree::Addresses::GeocodeAddressJob)
+
+ mail_1 = Spree::OrderMailer.deliveries.find { |delivery| delivery.to.include?(vendor.contact_person_email) }
+ vendor_order_1 = vendor.orders.last
+
+ expect(mail_1).to be_present
+ expect(mail_1.subject).to eq('New Order Received')
+ expect(mail_1.body).to include(vendor_order_1.number)
+
+ mail_2 = Spree::OrderMailer.deliveries.find { |delivery| delivery.to.include?(vendor_2.contact_person_email) }
+ vendor_order_2 = vendor_2.orders.last
+
+ expect(mail_2).to be_present
+ expect(mail_2.subject).to eq('New Order Received')
+ expect(mail_2.body).to include(vendor_order_2.number)
+ end
+
+ it 'stores correct totals' do
+ setup_order_with_everything
+
+ expect(parent_order.vendor_orders.count).to eq(0)
+
+ check_totals
+
+ subject.call(order: parent_order)
+ parent_order.reload
+
+ expect(parent_order.vendor_orders.count).to eq(2)
+ expect(parent_order.vendor_orders.pluck(:platform_fee_total)).to contain_exactly(vendor_platform_fee_total, vendor_2_platform_fee_total)
+ expect(parent_order.vendor_orders.pluck(:additional_tax_total)).to contain_exactly(vendor_additional_tax_total, vendor_2_additional_tax_total)
+
+ # vendor order includes no tax in order total
+ expect(parent_order.vendor_orders.pluck(:total)).to contain_exactly(vendor_order_total, vendor_2_order_total)
+
+ check_totals
+ end
+
+ it 'keeps taxes in the vendor order' do
+ setup_order_with_everything
+
+ expect(parent_order.vendor_orders.count).to eq(0)
+
+ check_totals
+
+ subject.call(order: parent_order)
+ parent_order.reload
+
+ expect(parent_order.vendor_orders.count).to eq(2)
+ expect(parent_order.vendor_order_line_item_adjustments.count).to eq(2)
+
+ expect(parent_order.vendor_order_line_item_adjustments.pluck(:amount)).to match_array [9.0, 4.9]
+
+ check_totals
+ end
+
+ private
+
+ def check_totals
+ expect(parent_order.item_total).to eq(order_item_total)
+ expect(parent_order.total).to eq(order_total)
+ expect(parent_order.adjustment_total).to eq(order_promo_total + order_additional_tax_total)
+ expect(parent_order.payment_total).to eq(order_total)
+ expect(parent_order.shipment_total).to eq(order_shipment_total)
+ expect(parent_order.additional_tax_total).to eq(order_additional_tax_total)
+ expect(parent_order.promo_total).to eq(order_promo_total)
+ expect(parent_order.included_tax_total).to eq(0.0)
+ expect(parent_order.taxable_adjustment_total).to eq(order_promo_total)
+ expect(parent_order.non_taxable_adjustment_total).to eq(0.0)
+ expect(parent_order.platform_fee_total).to eq(0.0)
+ end
+
+ def setup_order_with_everything
+ create_promotion
+ create_shipments
+ create_tax_rates
+ update_parent_order
+
+ create(:payment, order: parent_order, amount: parent_order.total, state: :completed)
+ end
+
+ def update_parent_order
+ parent_order.reload
+ parent_order.update_line_item_prices!
+ parent_order.create_tax_charge!
+ parent_order.update_with_updater!
+ end
+
+ def create_tax_rates
+ create(:tax_rate, amount: 0.1, tax_category: tax_category, zone: parent_order.tax_zone)
+ end
+
+ def create_shipments
+ create(:shipment, order: parent_order, cost: 10, stock_location: variant.stock_items.first.stock_location, vendor: vendor, state: 'ready')
+ create(:shipment, order: parent_order, cost: 10, stock_location: variant_3.stock_items.first.stock_location, vendor: vendor_2, state: 'ready')
+ end
+
+ def create_promotion
+ parent_order.update!(state: 'cart')
+ create(:promotion, :with_order_adjustment, weighted_order_adjustment_amount: 10, stores: [store]).activate(order: parent_order)
+ parent_order.update!(state: 'complete')
+ end
+end
diff --git a/spec/services/spree/refunds/calculate_additional_tax_spec.rb b/spec/services/spree/refunds/calculate_additional_tax_spec.rb
new file mode 100644
index 00000000..60a6a217
--- /dev/null
+++ b/spec/services/spree/refunds/calculate_additional_tax_spec.rb
@@ -0,0 +1,57 @@
+RSpec.describe Spree::Refunds::CalculateAdditionalTax do
+ subject(:calculate_additional_tax) { described_class.call(refund: refund).value }
+
+ let(:refund) { create(:refund, amount: 100, payment: order.payments.first, reimbursement: reimbursement) }
+
+ let!(:order) { create(:vendor_shipped_order, without_line_items: true) }
+ let!(:line_items) do
+ [
+ create(:vendor_line_item, order: order, price: 10, quantity: 2),
+ create(:vendor_line_item, order: order, price: 20, quantity: 3)
+ ]
+ end
+
+ let(:shipment) { order.shipments[0] }
+
+ let(:reimbursement) { nil }
+ let(:currency) { refund.currency }
+
+ let!(:tax_adjustment_1) { create(:tax_adjustment, adjustable: line_items[0], order: order, amount: 2) }
+ let!(:tax_adjustment_2) { create(:tax_adjustment, adjustable: line_items[1], order: order, amount: 6) }
+
+ context 'for a parent order' do
+ let!(:order) { create(:shipped_order, without_line_items: true) }
+ let!(:line_items) do
+ [
+ create(:line_item, order: order, price: 10, quantity: 2),
+ create(:line_item, order: order, price: 20, quantity: 3)
+ ]
+ end
+
+ it { is_expected.to eq(Spree::Money.new(0, currency: order.currency)) }
+ end
+
+ context 'with reimbursement' do
+ let(:reimbursement) { create(:reimbursement, return_items_count: 2) }
+
+ before do
+ reimbursement.return_items[0].inventory_unit.update!(quantity: 1, line_item_id: line_items[0].id)
+ reimbursement.return_items[1].inventory_unit.update!(quantity: 2, line_item_id: line_items[1].id)
+ end
+
+ it 'calculates the sales tax for returned items' do
+ expect(calculate_additional_tax).to eq(Spree::Money.new(5.0, currency: currency))
+ end
+ end
+
+ context 'without reimbursement' do
+ before do
+ order.line_items.reload
+ order.shipments[0].update_column(:additional_tax_total, 3)
+ end
+
+ it 'calculates the sales tax for all items' do
+ expect(calculate_additional_tax).to eq(Spree::Money.new(11.0, currency: currency))
+ end
+ end
+end
diff --git a/spec/services/spree_multi_vendor/fees/calculate_item_fee_spec.rb b/spec/services/spree_multi_vendor/fees/calculate_item_fee_spec.rb
new file mode 100644
index 00000000..a3b0186e
--- /dev/null
+++ b/spec/services/spree_multi_vendor/fees/calculate_item_fee_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::Fees::CalculateItemFee do
+ let(:line_item) do
+ create(:line_item,
+ price: 99.99,
+ quantity: 2,
+ promo_total: 0.00,
+ product: create(:product_in_stock, vendor: vendor),
+ vendor: vendor)
+ end
+ let(:rate) { nil }
+ let(:vendor) { create(:vendor, platform_fee: vendor_platform_fee) }
+ let(:vendor_platform_fee) { 10 }
+ let(:product_platform_fee) { 5 }
+
+ before { allow(vendor).to receive(:payment_gateway_connected?).and_return true }
+
+ describe '#call' do
+ subject { described_class.call(item: line_item, rate: rate) }
+ let(:result) { subject.value }
+
+ context 'if rate is not passed' do
+ context 'if product platform fee is present' do
+ before { line_item.product.update!(platform_fee: product_platform_fee) }
+
+ it 'sets line item platform fee per unit correctly' do
+ expect(result[:fee_per_unit]).to eq 5
+ end
+
+ it 'sets line item platform fee total correctly' do
+ expect(result[:fee_total]).to eq 10
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ expect(result[:rate]).to eq product_platform_fee
+ end
+ end
+
+ context 'if product platform fee is not present, but vendor platform fee is present' do
+ it 'sets line item platform fee per unit correctly' do
+ expect(result[:fee_per_unit]).to eq 10
+ end
+
+ it 'sets line item platform fee total correctly' do
+ expect(result[:fee_total]).to eq 20
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ expect(result[:rate]).to eq vendor_platform_fee
+ end
+ end
+
+ context 'if rate is zero' do
+ before { line_item.product.update!(platform_fee: 0.0) }
+
+ it 'returns failure' do
+ expect(subject.failure?).to be true
+ end
+ end
+ end
+
+ context 'if custom rate is passed' do
+ before { subject }
+
+ let(:rate) { 50.0 }
+
+ it 'sets line item platform fee per unit correctly' do
+ expect(result[:fee_per_unit]).to eq 50
+ end
+
+ it 'sets line item platform fee total correctly' do
+ expect(result[:fee_total]).to eq 100
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ expect(result[:rate]).to eq rate
+ end
+ end
+
+ context 'when line item is discounted' do
+ before do
+ line_item.update(promo_total: -10.0)
+ line_item.reload
+ end
+
+ it 'sets line item platform fee per unit correctly' do
+ expect(result[:fee_per_unit]).to eq 9.5
+ end
+
+ it 'sets line item platform fee total correctly' do
+ expect(result[:fee_total]).to eq 19
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ expect(result[:rate]).to eq vendor_platform_fee
+ end
+ end
+
+ context 'when store credit is applied to the order' do
+ let(:store) { Spree::Store.default }
+ let(:order) { create(:order, store: store) }
+
+ let(:other_vendor) { create(:vendor, platform_fee: vendor_platform_fee) }
+
+ let!(:line_item) { create(:line_item, order: order, product: product, price: 50, quantity: 2) }
+ let!(:line_item2) { create(:line_item, order: order, product: product2, price: 10, quantity: 2) }
+ let!(:line_item3) { create(:line_item, order: order, product: product3, price: 30, quantity: 1) }
+
+ let(:product) { create(:product_in_stock, vendor: vendor, price: 50) }
+ let(:product2) { create(:product_in_stock, vendor: vendor, price: 10) }
+ let(:product3) { create(:product_in_stock, vendor: other_vendor, price: 30) }
+
+ before do
+ order.update_with_updater!
+ create(:store_credit_payment, order: order, amount: 30)
+ end
+
+ it 'calculates store credit per unit correctly' do
+ # store_credit_per_unit = (50 / 150) * 30 = 10
+ expect(result[:store_credit_per_unit]).to eq(10)
+ end
+
+ it 'calculates fee per unit correctly after deducting store credit' do
+ # feeable_amount = 50 - 10 (store_credit) = 40
+ # fee_per_unit = 40 * 10% = 4
+ expect(result[:fee_per_unit]).to eq(4)
+ end
+
+ it 'calculates fee total correctly' do
+ # fee_total = 4 * 2 = 8
+ expect(result[:fee_total]).to eq(8)
+ end
+ end
+ end
+end
diff --git a/spec/services/spree_multi_vendor/fees/create_all_items_fees_spec.rb b/spec/services/spree_multi_vendor/fees/create_all_items_fees_spec.rb
new file mode 100644
index 00000000..b7964cce
--- /dev/null
+++ b/spec/services/spree_multi_vendor/fees/create_all_items_fees_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::Fees::CreateAllItemsFees do
+ subject { described_class.new(order: order.reload).call }
+
+ let!(:order) { create(:vendor_order_with_line_items, line_items_count: 3, vendor: vendor) }
+ let(:vendor) { create(:vendor, platform_fee: vendor_platform_fee) }
+ let(:vendor_platform_fee) { 10 }
+ let(:product_platform_fee) { 5 }
+ let(:fees_amount) { 15.0 }
+
+ let(:line_item_1) { order.line_items.first }
+ let(:line_item_2) { order.line_items.second }
+ let(:line_item_3) { order.line_items.third }
+
+ let(:fee_1) { subject.find { |fee| fee.feeable == line_item_1 } }
+ let(:fee_2) { subject.find { |fee| fee.feeable == line_item_2 } }
+ let(:fee_3) { subject.find { |fee| fee.feeable == line_item_3 } }
+
+ describe '#call' do
+ before do
+ allow(vendor).to receive(:payment_gateway_connected?).and_return true
+
+ order.line_items.update_all(vendor_id: vendor.id, price: 99.99)
+
+ line_item_1.product.update!(vendor: vendor, platform_fee: product_platform_fee)
+ line_item_2.product.update!(vendor: vendor)
+ line_item_3.product.update!(vendor: vendor)
+ end
+
+ it 'creates application fee adjustments' do
+ subject
+
+ expect(fee_1.amount).to eq(5)
+ expect(fee_1.label).to eq Spree::MarketplaceCommission::ITEM_LABEL
+ expect(fee_1.rate).to eq product_platform_fee
+ expect(fee_1.feeable.platform_fee_per_unit).to eq(5)
+ expect(fee_1.feeable.platform_fee_total).to eq(5)
+
+ expect(fee_2.amount).to eq(10)
+ expect(fee_2.label).to eq Spree::MarketplaceCommission::ITEM_LABEL
+ expect(fee_2.rate).to eq vendor_platform_fee
+ expect(fee_2.feeable.platform_fee_per_unit).to eq(10)
+ expect(fee_2.feeable.platform_fee_total).to eq(10)
+
+ expect(fee_3.amount).to eq(10)
+ expect(fee_3.label).to eq Spree::MarketplaceCommission::ITEM_LABEL
+ expect(fee_3.rate).to eq vendor_platform_fee
+ expect(fee_3.feeable.platform_fee_per_unit).to eq(10)
+ expect(fee_3.feeable.platform_fee_total).to eq(10)
+ end
+
+ context 'for non vendorized items' do
+ before do
+ order.line_items.second.update_column(:vendor_id, nil)
+ end
+
+ it 'does not call CreateItemFee' do
+ expect { subject }.to change(Spree::MarketplaceCommission, :count).by(2)
+
+ expect(fee_1).to be_present
+ expect(fee_2).to be_blank
+ expect(fee_3).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/spree_multi_vendor/fees/create_item_fee_spec.rb b/spec/services/spree_multi_vendor/fees/create_item_fee_spec.rb
new file mode 100644
index 00000000..4e07f142
--- /dev/null
+++ b/spec/services/spree_multi_vendor/fees/create_item_fee_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::Fees::CreateItemFee do
+ subject { described_class.new(item: line_item, rate: rate).call }
+
+ let(:line_item) { create(:line_item, price: 99.99, quantity: 2) }
+ let(:rate) { nil }
+ let(:vendor) { create(:vendor, platform_fee: vendor_platform_fee) }
+ let(:vendor_platform_fee) { 10 }
+ let(:product_platform_fee) { 5 }
+
+ before do
+ line_item.product.update!(vendor: vendor)
+ line_item.update!(vendor: vendor)
+
+ allow(vendor).to receive(:payment_gateway_connected?).and_return true
+ end
+
+ describe '#call' do
+ context 'if rate is not passed' do
+ context 'if product platform fee is present' do
+ before do
+ line_item.product.update!(platform_fee: product_platform_fee)
+ end
+
+ it 'calculates rate based on product platform fee and creates a fee' do
+ expect { subject }.to change { line_item.marketplace_commissions.count }.by(1)
+ expect(subject.amount).to eq(10)
+ expect(subject.label).to eq Spree::MarketplaceCommission::ITEM_LABEL
+ expect(subject.feeable).to eq(line_item)
+ end
+
+ it 'sets line item platform fee per unit correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_per_unit).to eq(5)
+ end
+
+ it 'sets line item platform fee total correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_total).to eq(10)
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_rate).to eq product_platform_fee
+ end
+ end
+
+ context 'if product platform fee is not present' do
+ it 'calculates rate based on vendor platform fee and creates a fee' do
+ expect { subject }.to change { line_item.marketplace_commissions.count }.by(1)
+ expect(subject.amount).to eq(20)
+ expect(subject.label).to eq Spree::MarketplaceCommission::ITEM_LABEL
+ expect(subject.feeable).to eq(line_item)
+ end
+
+ it 'sets line item platform fee per unit correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_per_unit).to eq(10)
+ end
+
+ it 'sets line item platform fee total correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_total).to eq(20)
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_rate).to eq vendor_platform_fee
+ end
+ end
+
+ context 'if rate is zero' do
+ before do
+ line_item.product.update!(platform_fee: 0.0)
+ end
+
+ it 'does not create a platform fee' do
+ expect { subject }.not_to change { line_item.marketplace_commissions.count }
+ end
+
+ it 'sets line item platform fee per unit correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_per_unit).to eq 0.0
+ end
+
+ it 'sets line item platform fee total correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_total).to eq 0.0
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_rate).to eq nil
+ end
+ end
+ end
+
+ context 'if custom rate is passed' do
+ let(:rate) { 50.0 }
+
+ it 'calculates rate based on custom_rate and creates a fee' do
+ expect { subject }.to change { line_item.marketplace_commissions.count }.by(1)
+ expect(subject.amount).to eq(100)
+ expect(subject.label).to eq Spree::MarketplaceCommission::ITEM_LABEL
+ expect(subject.feeable).to eq(line_item)
+ end
+
+ it 'sets line item platform fee per unit correctly' do
+ subject
+
+ expect(line_item.reload.platform_fee_per_unit).to eq(50)
+ end
+
+ it 'sets line item platform fee total correctly' do
+ subject
+
+ expect(line_item.platform_fee_total).to eq(100)
+ end
+
+ it 'sets line item platform fee rate correctly' do
+ subject
+
+ expect(line_item.platform_fee_rate).to eq(rate)
+ end
+ end
+ end
+end
diff --git a/spec/services/spree_multi_vendor/fees/create_reverse_item_fees_spec.rb b/spec/services/spree_multi_vendor/fees/create_reverse_item_fees_spec.rb
new file mode 100644
index 00000000..1d30fd61
--- /dev/null
+++ b/spec/services/spree_multi_vendor/fees/create_reverse_item_fees_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::Fees::CreateReverseItemFees do
+ subject { described_class.new(line_item: line_item, inventory_unit: inventory_unit).call }
+
+ let(:return_item) { create(:return_item) }
+ let(:inventory_unit) { return_item.inventory_unit }
+ let(:line_item) { inventory_unit.line_item }
+ let(:item_price) { 50.0 }
+
+ describe '#call' do
+ let(:platform_fee_total) { 15.0 }
+
+ before do
+ line_item.order.update!(platform_fee_reverse_total: 8.5)
+ line_item.update!(
+ price: item_price,
+ platform_fee_rate: 10.0,
+ platform_fee_per_unit: 5.0,
+ platform_fee_total: platform_fee_total,
+ platform_fee_reverse_total: 8.5
+ )
+
+ inventory_unit.update!(quantity: 3)
+ end
+
+ it 'creates item reverse platform fee' do
+ expect { subject }.to change { line_item.marketplace_commissions.count }.by(1)
+
+ reverse_fee = line_item.marketplace_commissions.last
+
+ expect(reverse_fee.amount).to eq 15.0
+ expect(reverse_fee.label).to eq Spree::MarketplaceCommission::REVERSE_ITEM_LABEL
+ expect(reverse_fee.rate).to eq 10.0
+
+ expect(line_item.reload.platform_fee_reverse_total).to eq(23.5)
+ expect(line_item.order.reload.platform_fee_reverse_total).to eq(23.5)
+ end
+
+ it 'does not adjust line item platform fee total' do
+ subject
+ expect(line_item.reload.platform_fee_total).to eq(15)
+ end
+
+
+ context 'if line item platform fee is zero' do
+ let(:platform_fee_total) { 0 }
+
+ it 'does not create reverse platform fee' do
+ expect { subject }.not_to change { line_item.marketplace_commissions.count }
+ expect(subject).to eq nil
+ end
+ end
+ end
+end
diff --git a/spec/services/spree_multi_vendor/fees/create_reverse_order_fee_spec.rb b/spec/services/spree_multi_vendor/fees/create_reverse_order_fee_spec.rb
new file mode 100644
index 00000000..80cf2f80
--- /dev/null
+++ b/spec/services/spree_multi_vendor/fees/create_reverse_order_fee_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::Fees::CreateReverseOrderFee do
+ describe '#call' do
+ subject { described_class.new(order: order, refunded_amount: refunded_amount).call }
+
+ let(:order) { create(:order, platform_fee_total: 20) }
+ let(:refunded_amount) { 10 }
+
+ before do
+ order.update!(
+ total: 100.0,
+ platform_fee_reverse_total: 3.5
+ )
+ end
+
+ it 'creates reversed platform fee linked to the order' do
+ expect { subject }.to change { order.marketplace_commissions.count }.by(1)
+
+ expect(subject.amount).to eq(2.0)
+ expect(order.reload.platform_fee_reverse_total).to eq(5.5)
+ end
+
+ it 'does not adjust line item platform fee total' do
+ subject
+ expect(order.platform_fee_total).to eq(20)
+ end
+
+ context 'if order platform fee is 0.0' do
+ let(:order) { create(:order, platform_fee_total: 0.0) }
+
+ it 'does not create reverse platform fee' do
+ expect { subject }.not_to change { order.marketplace_commissions.count }
+ expect(subject).to eq nil
+ end
+ end
+
+ context 'when trying to refund more commission than allowed' do
+ before do
+ order.update!(platform_fee_reverse_total: 18.5)
+ end
+
+ it 'creates reversed platform fee linked to the order' do
+ expect { subject }.to change { order.marketplace_commissions.count }.by(1)
+
+ expect(subject.amount).to eq(1.5)
+ expect(order.reload.platform_fee_reverse_total).to eq(20.0)
+ end
+ end
+ end
+end
diff --git a/spec/services/spree_multi_vendor/store_credits/calculate_vendor_store_credit_spec.rb b/spec/services/spree_multi_vendor/store_credits/calculate_vendor_store_credit_spec.rb
new file mode 100644
index 00000000..22b0f5db
--- /dev/null
+++ b/spec/services/spree_multi_vendor/store_credits/calculate_vendor_store_credit_spec.rb
@@ -0,0 +1,118 @@
+require 'spec_helper'
+
+RSpec.describe SpreeMultiVendor::StoreCredits::CalculateVendorStoreCredit do
+ subject(:calculate_vendor_store_credit) { described_class.call(item_total: item_total, order: order, store_credit_total: store_credit_total) }
+
+ let(:result) { calculate_vendor_store_credit.value }
+ let(:store_credit_total) { nil }
+
+ let(:store) { Spree::Store.default }
+ let(:vendors) { create_list(:vendor, 2) }
+
+ let(:order) { create(:order, store: store) }
+
+ let(:product1) { create(:product_in_stock, vendor: vendors[0], price: product1_price) }
+ let(:product2) { create(:product_in_stock, vendor: vendors[1], price: product2_price) }
+
+ let(:product1_price) { 40 }
+ let(:product2_price) { 30 }
+
+ before do
+ create(:line_item, order: order, product: product1, price: product1_price, quantity: 2)
+ create(:line_item, order: order, product: product2, price: product2_price, quantity: 4)
+
+ order.update_with_updater!
+ end
+
+ context 'with store credit payment' do
+ before do
+ create(:store_credit_payment, order: order, amount: 60)
+ end
+
+ context 'for a partial item total' do
+ let(:item_total) { 80 }
+
+ it { is_expected.to be_success }
+
+ it 'calculates vendor store credit proportionally' do
+ # vendor_store_credit = (80 / 200) * 60 = 24
+ expect(result).to eq(24)
+ end
+ end
+
+ context 'when item total is zero' do
+ let(:item_total) { 0 }
+
+ it { is_expected.to be_success }
+
+ it 'returns zero' do
+ expect(result).to eq(0)
+ end
+ end
+ end
+
+ context 'with no store credit payment' do
+ let(:item_total) { 80 }
+
+ it { is_expected.to be_success }
+
+ it 'returns zero' do
+ expect(result).to eq(0)
+ end
+ end
+
+ context 'when custom store credit total is passed' do
+ let(:store_credit_total) { 40 }
+
+ context 'for full item total' do
+ let(:item_total) { 200 }
+
+ it { is_expected.to be_success }
+
+ it 'calculates vendor store credit using custom total' do
+ # vendor_store_credit = (200 / 200) * 40 = 40
+ expect(result).to eq(40)
+ end
+ end
+
+ context 'for partial item total' do
+ let(:item_total) { 80 }
+
+ it { is_expected.to be_success }
+
+ it 'calculates vendor store credit proportionally using custom total' do
+ # vendor_store_credit = (80 / 200) * 40 = 16
+ expect(result).to eq(16)
+ end
+ end
+
+ context 'when custom store credit total is zero' do
+ let(:item_total) { 80 }
+ let(:store_credit_total) { 0 }
+
+ it { is_expected.to be_success }
+
+ it 'returns zero' do
+ expect(result).to eq(0)
+ end
+ end
+ end
+
+ context 'when calculation results in many decimal places' do
+ let(:product3) { create(:product_in_stock, vendor: vendors[1], price: product3_price) }
+ let(:product3_price) { 33.33 }
+
+ before do
+ create(:line_item, order: order, product: product3, price: product3_price, quantity: 3)
+ order.reload.update_with_updater!
+ end
+
+ let(:item_total) { 99.99 }
+ let(:store_credit_total) { 17.89 }
+
+ it 'rounds to 2 decimal places' do
+ # vendor_store_credit = (99.99 / 299.99) * 17.89 = 5.96292...
+ expect(result).to eq(5.96)
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4e50745b..c255db3f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,13 +1,40 @@
- # Configure Rails Environment
+# Configure Rails Environment
ENV['RAILS_ENV'] = 'test'
+require 'dotenv/load'
+
require File.expand_path('../dummy/config/environment.rb', __FILE__)
-require 'shoulda/matchers'
require 'spree_dev_tools/rspec/spec_helper'
+require 'spree_multi_vendor/factories'
+
+require 'cancan/matchers'
+require 'spree/testing_support/ability_helpers'
+
+require 'spree/testing_support/next_instance_of'
+require 'spree/testing_support/capybara_config'
+
+require 'spree/admin/testing_support/capybara_utils'
+
+require 'spree_multi_vendor/testing_support/authorization_helpers'
+
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].sort.each { |f| require f }
-ActiveJob::Base.queue_adapter = :inline
\ No newline at end of file
+def json_response
+ case body = JSON.parse(response.body)
+ when Hash
+ body.with_indifferent_access
+ when Array
+ body
+ end
+end
+
+RSpec.configure do |config|
+ config.include Spree::Admin::TestingSupport::CapybaraUtils
+
+ config.example_status_persistence_file_path = '.rspec_status'
+ Capybara.test_id = 'data-test-id'
+end
diff --git a/spec/support/shoulda_matchers.rb b/spec/support/shoulda_matchers.rb
deleted file mode 100644
index 7d045f35..00000000
--- a/spec/support/shoulda_matchers.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-Shoulda::Matchers.configure do |config|
- config.integrate do |with|
- with.test_framework :rspec
- with.library :rails
- end
-end
diff --git a/spec/support/vendor_current_order.rb b/spec/support/vendor_current_order.rb
new file mode 100644
index 00000000..2a086c01
--- /dev/null
+++ b/spec/support/vendor_current_order.rb
@@ -0,0 +1,87 @@
+shared_context 'creates vendor order with line item' do
+ let!(:line_item) { create(:vendor_line_item, order: order, currency: currency, vendor: vendor) }
+ let!(:headers) { headers_bearer }
+
+ before do
+ order.reload
+ ensure_order_totals
+ end
+end
+
+shared_context 'vendor order with a physical line item' do
+ include_context 'creates vendor order with line item'
+end
+
+shared_context 'vendor order with a digital line item' do
+ let!(:digital) { create(:digital, variant: create(:product_in_stock, vendor: vendor).default_variant) }
+ let!(:variant_digital) { digital.variant }
+ let!(:line_item) { create(:vendor_line_item, variant: variant_digital, order: order, currency: currency) }
+ let!(:headers) { headers_bearer }
+
+ before do
+ order.reload
+ ensure_order_totals
+ end
+end
+
+shared_context 'vendor order with a physical and digital line item' do
+ let!(:digital) { create(:digital, variant: create(:product_in_stock, vendor: vendor).default_variant) }
+ let!(:variant_digital) { digital.variant }
+ let!(:digital_line_item) { create(:vendor_line_item, vendor: vendor, variant: variant_digital, order: order, currency: currency) }
+ let!(:physical_line_item) { create(:vendor_line_item, vendor: vendor, order: order, currency: currency) }
+
+ let!(:headers) { headers_bearer }
+
+ before do
+ order.reload
+ ensure_order_totals
+ end
+end
+
+shared_context 'creates vendor guest order with guest token' do
+ let(:guest_token) { 'guest_token' }
+ let!(:order) { create(:order, token: guest_token, store: store, currency: currency) }
+ let!(:line_item) { create(:vendor_line_item, order: order, currency: currency, vendor: vendor) }
+ let!(:headers) { headers_order_token }
+
+ before { ensure_order_totals }
+end
+
+shared_examples 'returns valid cart JSON' do
+ it 'returns a valid cart JSON response' do
+ order.reload
+ expect(json_response['data']).to be_present
+ expect(json_response['data']).to have_id(order.id.to_s)
+ expect(json_response['data']).to have_type('cart')
+ expect(json_response['data']).to have_attribute(:number).with_value(order.number)
+ expect(json_response['data']).to have_attribute(:state).with_value(order.state)
+ expect(json_response['data']).to have_attribute(:payment_state).with_value(order.payment_state)
+ expect(json_response['data']).to have_attribute(:shipment_state).with_value(order.shipment_state)
+ expect(json_response['data']).to have_attribute(:token).with_value(order.token)
+ expect(json_response['data']).to have_attribute(:total).with_value(order.total.to_s)
+ expect(json_response['data']).to have_attribute(:item_total).with_value(order.item_total.to_s)
+ expect(json_response['data']).to have_attribute(:ship_total).with_value(order.ship_total.to_s)
+ expect(json_response['data']).to have_attribute(:adjustment_total).with_value(order.adjustment_total.to_s)
+ expect(json_response['data']).to have_attribute(:included_tax_total).with_value(order.included_tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:additional_tax_total).with_value(order.additional_tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_additional_tax_total).with_value(order.display_additional_tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_included_tax_total).with_value(order.display_included_tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:tax_total).with_value(order.tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:currency).with_value(order.currency.to_s)
+ expect(json_response['data']).to have_attribute(:email).with_value(order.email)
+ expect(json_response['data']).to have_attribute(:display_item_total).with_value(order.display_item_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_ship_total).with_value(order.display_ship_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_adjustment_total).with_value(order.display_adjustment_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_tax_total).with_value(order.display_tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:item_count).with_value(order.item_count)
+ expect(json_response['data']).to have_attribute(:special_instructions).with_value(order.special_instructions)
+ expect(json_response['data']).to have_attribute(:promo_total).with_value(order.promo_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_promo_total).with_value(order.display_promo_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_total).with_value(order.display_total.to_s)
+ expect(json_response['data']).to have_attribute(:pre_tax_item_amount).with_value(order.pre_tax_item_amount.to_s)
+ expect(json_response['data']).to have_attribute(:display_pre_tax_item_amount).with_value(order.display_pre_tax_item_amount.to_s)
+ expect(json_response['data']).to have_attribute(:pre_tax_total).with_value(order.pre_tax_total.to_s)
+ expect(json_response['data']).to have_attribute(:display_pre_tax_total).with_value(order.display_pre_tax_total.to_s)
+ expect(json_response['data']).to have_relationships(:user, :line_items, :variants, :billing_address, :shipping_address, :payments, :shipments, :promotions)
+ end
+end
diff --git a/spec/zeitwerk_spec.rb b/spec/zeitwerk_spec.rb
new file mode 100644
index 00000000..59b6c709
--- /dev/null
+++ b/spec/zeitwerk_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe 'Zeitwerk' do
+ it 'eager loads all files' do
+ expect do
+ Zeitwerk::Loader.eager_load_all
+ end.not_to raise_error
+ end
+end
diff --git a/spree_multi_vendor.gemspec b/spree_multi_vendor.gemspec
index b0c473a6..a9e49e20 100644
--- a/spree_multi_vendor.gemspec
+++ b/spree_multi_vendor.gemspec
@@ -7,36 +7,25 @@ require 'spree_multi_vendor/version'
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'spree_multi_vendor'
- s.version = SpreeMultiVendor.version
- s.summary = 'Spree Commerce multi vendor marketplace extension'
- s.description = 'Spree Commerce multi vendor marketplace extension'
+ s.version = SpreeMultiVendor::VERSION
+ s.summary = "Spree Commerce Multi vendor Extension"
s.required_ruby_version = '>= 3.0'
- s.author = 'Spark Solutions'
- s.email = 'we@sparksolutions.co'
- s.homepage = 'https://spreecommerce.org/use-cases/multi-vendor-marketplace-demo/'
- s.license = 'BSD-3-Clause'
+ s.authors = ['Vendo Connect Inc.', 'Vendo Sp. z o.o.']
+ s.email = 'hello@spreecommerce.org'
+ s.homepage = 'https://github.com/spree/spree_multi_vendor'
- s.metadata = {
- 'bug_tracker_uri' => 'https://github.com/spree-contrib/spree_multi_vendor/issues',
- 'changelog_uri' => "https://github.com/spree-contrib/spree_multi_vendor/releases/tag/v#{s.version}",
- 'documentation_uri' => 'https://guides.spreecommerce.org/',
- 'source_code_uri' => "https://github.com/spree-contrib/spree_multi_vendor/tree/v#{s.version}",
- }
-
- s.files = `git ls-files`.split("\n")
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.files = Dir["{app,config,db,lib,vendor}/**/*", "LICENSE.md", "Rakefile", "README.md"].reject { |f| f.match(/^spec/) && !f.match(/^spec\/fixtures/) }
s.require_path = 'lib'
s.requirements << 'none'
- spree_version = '>= 4.7.0'
+ spree_version = '>= 5.2.0'
s.add_dependency 'spree', spree_version
- s.add_dependency 'spree_backend', spree_version
- s.add_dependency 'spree_emails', spree_version
- s.add_dependency 'spree_extension'
+ s.add_dependency 'spree_admin', spree_version
- s.add_dependency 'deface', '~> 1.0'
+ s.add_dependency 'spree_extension'
- s.add_development_dependency 'shoulda-matchers'
+ s.add_development_dependency 'email_spec'
s.add_development_dependency 'spree_dev_tools'
+ s.add_development_dependency 'timecop'
end
diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/stylesheets/.keep b/vendor/stylesheets/.keep
new file mode 100644
index 00000000..e69de29b