Разработка кастомного расширения Spree Commerce

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомного расширения Spree Commerce
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка кастомного расширения Spree Commerce

Spree расширяется через Rails Engine — это отдельный gem с собственными моделями, контроллерами, вьюхами и миграциями. Механизм тот же, что и у самого Spree: Rails Engine + Decorator pattern для изменения поведения существующих классов. Можно упаковать расширение в gem для переиспользования или держать код непосредственно в основном приложении.

Два подхода к расширению

Inline (в приложении): Декораторы в app/models/spree/, переопределение вьюх через deface, новые контроллеры. Подходит для специфичных доработок.

Rails Engine (gem): Полноценный gem с Engine, который монтируется в основное приложение. Подходит для переиспользуемой функциональности — например, плагин для конкретного платёжного провайдера.

Структура расширения как Engine

bundle gem spree_b2b_pricing --no-ext
spree_b2b_pricing/
├── app/
│   ├── models/
│   │   └── spree/
│   │       ├── b2b_price_list.rb
│   │       └── order_decorator.rb
│   ├── controllers/
│   │   └── spree/
│   │       └── api/v2/
│   │           └── storefront/
│   │               └── price_lists_controller.rb
│   └── views/
│       └── spree/
│           └── admin/
├── config/
│   ├── routes.rb
│   └── initializers/
│       └── spree_b2b_pricing.rb
├── db/
│   └── migrate/
│       └── 20240101000000_create_spree_b2b_price_lists.rb
├── lib/
│   ├── spree_b2b_pricing.rb
│   └── spree_b2b_pricing/
│       ├── engine.rb
│       └── version.rb
└── spree_b2b_pricing.gemspec

Engine

# lib/spree_b2b_pricing/engine.rb
module SpreeB2bPricing
  class Engine < ::Rails::Engine
    require "spree/core"
    isolate_namespace SpreeB2bPricing

    config.autoload_paths += %W[#{config.root}/lib]

    initializer "spree_b2b_pricing.register_hooks" do
      Spree::Config.configure do |config|
        # Регистрация кастомного калькулятора цен
      end
    end

    def self.activate
      Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator.rb")) do |c|
        Rails.configuration.cache_classes ? require(c) : load(c)
      end
    end

    config.to_prepare(&method(:activate))
  end
end

Decorator: расширение модели Spree::Order

# app/models/spree/order_decorator.rb
module Spree
  module OrderDecorator
    def self.prepended(base)
      base.belongs_to :b2b_price_list,
                      class_name: "Spree::B2bPriceList",
                      optional: true
    end

    # Переопределяем метод пересчёта цен
    def update_line_item_prices!
      return super unless b2b_price_list

      line_items.each do |line_item|
        b2b_price = b2b_price_list.price_for(
          line_item.variant,
          currency
        )
        if b2b_price
          line_item.price = b2b_price
          line_item.save!
        end
      end

      super
    end

    def applicable_promotions
      return super unless user&.b2b_customer?
      # B2B клиенты не получают публичные промокоды
      super.where(b2b_only: true)
    end
  end
end

Spree::Order.prepend(Spree::OrderDecorator)

Переопределение вьюх через Deface

# app/overrides/spree/admin/products/_form_override.rb
Deface::Override.new(
  virtual_path: "spree/admin/products/_form",
  name: "add_b2b_wholesale_price",
  insert_after: "[data-hook='product_form_right']",
  text: <<~HTML
    <div data-hook="b2b_wholesale_price">
      <%= f.field_container :wholesale_price do %>
        <%= f.label :wholesale_price, "Оптовая цена (руб.)" %>
        <%= f.text_field :wholesale_price, class: "form-control" %>
      <% end %>
    </div>
  HTML
)

Deface не модифицирует файлы Spree — он применяет патчи в рантайме, что делает обновления Spree безопасными.

Расширение Admin API

# app/controllers/spree/admin/b2b_price_lists_controller.rb
module Spree
  module Admin
    class B2bPriceListsController < Spree::Admin::ResourceController
      before_action :load_resource

      def create
        @b2b_price_list = Spree::B2bPriceList.new(b2b_price_list_params)
        if @b2b_price_list.save
          flash[:success] = "Прайс-лист создан"
          redirect_to admin_b2b_price_lists_path
        else
          render :new
        end
      end

      private

      def b2b_price_list_params
        params.require(:b2b_price_list).permit(:name, :discount_percent, :active)
      end
    end
  end
end

Расширение Storefront API v2

# app/controllers/spree/api/v2/storefront/b2b_controller.rb
module Spree
  module Api
    module V2
      module Storefront
        class B2bController < ::Spree::Api::V2::BaseController
          before_action :require_spree_current_user

          def price_list
            user = spree_current_user
            price_list = Spree::B2bPriceList.for_user(user)
            render_serialized_payload { serialize_resource(price_list) }
          end
        end
      end
    end
  end
end
# config/routes.rb
Spree::Core::Engine.routes.draw do
  namespace :api do
    namespace :v2 do
      namespace :storefront do
        resource :b2b, only: [:show] do
          get :price_list
        end
      end
    end
  end

  namespace :admin do
    resources :b2b_price_lists
  end
end

Миграция

# db/migrate/20240101000000_create_spree_b2b_price_lists.rb
class CreateSpreeB2bPriceLists < ActiveRecord::Migration[7.1]
  def change
    create_table :spree_b2b_price_lists do |t|
      t.string :name, null: false
      t.decimal :discount_percent, precision: 5, scale: 2
      t.boolean :active, default: true, null: false
      t.timestamps
    end

    add_column :spree_orders, :b2b_price_list_id, :bigint
    add_foreign_key :spree_orders, :spree_b2b_price_lists,
                    column: :b2b_price_list_id
    add_index :spree_orders, :b2b_price_list_id
  end
end

Тестирование расширения

# spec/models/spree/order_decorator_spec.rb
RSpec.describe Spree::Order do
  describe "#update_line_item_prices!" do
    let(:price_list) { create(:b2b_price_list, discount_percent: 10) }
    let(:order) { create(:order_with_line_items, b2b_price_list: price_list) }

    it "applies b2b pricing to line items" do
      original_price = order.line_items.first.price
      order.update_line_item_prices!
      expect(order.line_items.first.reload.price)
        .to be_within(0.01).of(original_price * 0.9)
    end
  end
end

Типичные задачи и сроки

Расширение Срок
Кастомный калькулятор доставки 1–2 дня
B2B ценообразование 3–5 дней
Программа лояльности 4–6 дней
Кастомный платёжный gateway 2–4 дня
Расширение Admin UI (новые разделы) 2–3 дня