mcpField
S
spree-dependencies

spree-dependencies

github.com/spreeDevelopment10219 installsUpdated 4w ago
Repository

About spree-dependencies

A developer skill explaining how to replace Spree core classes through dependency injection. It helps Spree app and extension developers choose between hooks and class swaps, and documents contracts for workflows, services, and API serializers.

Key features

  1. 01Identify when a class swap is appropriate
  2. 02Configure workflow and service replacements
  3. 03Extend API serializers through dependency keys
  4. 04Handle legacy dependency keys
  5. 05Call Spree accessors to respect overrides

Use cases

  1. 01Implement a custom cart or order workflow
  2. 02Add fields to Store or Admin API serializers
  3. 03Migrate a legacy service override to a workflow
  4. 04Ensure extension code uses host application overrides

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/spree/agent-skills --skill spree-dependencies

After installing, type/spree-dependenciesin Claude Code to confirm it loaded.

Details

From the project README

Spree Dependencies (swapping whole classes)

Commands use the Spree CLI (spree …). On a classic Rails app without the CLI, run bin/rails … / bundle exec rake … from the app root. Paths assume the Rails app lives in server/.

Spree.dependencies lets you replace a core class — a service, a workflow, a policy, a serializer — with your own. Spree's code calls Spree.cart_add_item_workflow.call(...), never Spree::Carts::AddItem.call(...), so your replacement runs everywhere: Store API, Admin API, dashboard, jobs, imports.

First: do you actually need to replace the class?

Replacing a class means you own a copy that must be re-synced with every Spree release. Most reasons people swap a class are covered by a workflow hook, which survives upgrades:

You want to…Use instead of a swap
Block add-to-cart / checkout / a cancel / a returnvalidate hook — Spree.hooks.register('carts.add_item.validate', 'MyStore::Rule')
Add data to tax or promotion calculationContext hook (carts.recalculate_totals.set_tax_line_context, carts.recalculate.set_promotion_context)
Write rows alongside the change, atomicallyLifecycle hook (carts.add_item.after_item_added, carts.complete.after_finalize)
Sync to ERP / send email / reindexEvent subscriber (spree-events-webhooks)
Add a checkout requirementSpree::Checkout::Registry.add_requirement (spree-checkout)
Add a field to an API responseSubclass the serializer (below) — a small, legitimate swap

See the spree-workflows skill for the hook catalog. Swap a class only when you are genuinely replacing how something is computed (a different merge strategy, a custom buy-box ranking, a B2B access policy, a commission model).

The seams

Spree::Core::Dependencies::INJECTION_POINTS_WITH_DEFAULTS (in spree_core, lib/spree/core/dependencies.rb) defines 167 core injection points; 106 of them are *_workflow keys. Spree::Api::ApiDependencies::INJECTION_POINTS_WITH_DEFAULTS (spree_api, lib/spree/api/dependencies.rb) holds 272 API points — serializers only: 91 Store (product_serializer), 139 Admin (admin_product_serializer), 42 Seller (seller_*_serializer). (Counts from the 6.0 source; run spree rake spree:dependencies:list for your version.)

Naming tells you the contract:

SuffixBase classYou override
*_workflowSpree::Workflow subclass#perform(**keywords) — never #call
*_serviceplain class with prepend Spree::ServiceModule::Base#call(**keywords)
*_class, *_finder, *_handler, othersvaries — read the defaultas documented on the default

Frequently swapped:

KeyDefaultNotes
cart_add_item_workflowSpree::Carts::AddItemperform(variant:, cart:, quantity:, metadata:, options:, price:)
cart_upsert_items_workflowSpree::Carts::UpsertItemsset-quantity / remove / bulk
cart_recalculate_workflowSpree::Carts::Recalculatefull reprice (delivery, promotions, totals)
cart_recalculate_totals_workflowSpree::Carts::RecalculateTotalsthe single totals seam (discount/fee/tax rows → sums)
order_recalculate_totals_workflowSpree::Orders::RecalculateTotalspost-placement re-sum twin
cart_merge_workflowSpree::Carts::Mergeguest→customer cart merge
carts_complete_workflowSpree::Carts::Completecheckout completion — prefer carts.complete.* hooks
order_complete_workflow / order_cancel_workflowSpree::Orders::Complete / ::Cancel
order_update_statuses_serviceSpree::Orders::UpdateStatusessole writer of payment_status / fulfillment_status
fulfillment_create_workflow / fulfillment_update_workflowSpree::Fulfillments::Create / ::Update
payment_process_workflow, payment_capture_workflow, payment_void_workflow, refund_create_workflowSpree::Payments::*, Spree::Refunds::Create
return_create_workflow … claim_*_workflowSpree::Returns::*, Exchanges::*, Claims::*
storefront_access_policy_classSpree::Storefront::AccessPolicyStore API record visibility (ownership + guest tokens); the B2B seam
products_for_context_serviceSpree::Products::ForContextcatalog narrowing for a buyer
product_buy_box_serviceSpree::Products::SelectBuyBoxmarketplace: which variant leads
purchase_amount_due_at_checkout_serviceSpree::Purchases::AmountDueAtCheckoutdeposits / net terms
tax_resolve_exemptions_serviceSpree::Tax::ResolveExemptionsexemption evidence for the tax provider
reporting_adapterSpree::Reporting::Adapters::Live
search_product_presenternil (set by a search gem)

ability_class (Spree::Ability) builds every staff CanCanCan ability — Admin API JWT staff, the Seller API, imports/exports. Subclass Spree::Ability, keep initialize(user, options = {}) (options[:store], options[:resource]), call super, then add rules. It works behind the permission-key gate, and secret API keys (Spree::ApiKeyAbility) never consult it. See spree-auth-permissions.

Replacing a workflow

Subclass the shipped workflow and override perform with the same keywords. perform's signature is the contract (Ruby raises ArgumentError on a mismatch); a bare super runs the whole parent flow and returns its Result.

# server/app/workflows/my_store/carts/add_item.rb
module MyStore
  module Carts
    class AddItem < Spree::Carts::AddItem
      # REQUIRED: without this the subclass dispatches 'my_store.carts.add_item.*'
      # and every handler registered on 'carts.add_item.*' silently stops firing.
      workflow_key 'carts.add_item'

      def perform(variant:, cart: nil, **rest)
        result = super                       # full Spree flow; a failure raises through, so code below runs only on success

        external_step :notify_recommendations # network I/O — outside any transaction
        result                                # Result#value is the line item
      end

      private

      def notify_recommendations
        MyStore::Recommendations.item_added(cart_id: cart.prefixed_id, sku: line_item.sku)
      end
    end
  end
end
# server/config/initializers/spree.rb
Spree.cart_add_item_workflow = MyStore::Carts::AddItem

Rules for workflow replacements:

  • Override perform, never call. Spree::Workflow#call owns instrumentation, failure/halt! handling and compensation.
  • Keep workflow_key pointing at the parent's key (see above). This is the most common silent breakage.
  • To change one piece, override the private step method (e.g. add_to_line_item) and call super — but step names are internals and can change between releases; a hook is safer.
  • Return a Spree::ServiceModule::Result (super's, or success(value)); a non-Result return is wrapped in success.
  • Callers of Spree.cart_add_item_workflow expect the parent's Result#value type (a line item here). Don't change it.
  • Workflows collaborate through other seams (step :recalculate, with: -> { Spree.cart_recalculate_workflow }), so swapping one seam affects every flow that delegates to it.

Replacing a plain service

Same pattern, overriding call:

module MyStore
  class CartUpdate < Spree::Carts::Update
    def call(cart:, params:)
      result = super
      MyStore::ErpSync.enqueue(result.value.prefixed_id) if result.success?
      result
    end
  end
end

Spree.carts_update_service = MyStore::CartUpdate

Inherit from the default so you keep Spree::ServiceModule::Base wiring (class-level .call, success/failure). Always return a Result.

Legacy *_service keys are stashed, not applied

Seams that became workflows still accept the old names so apps boot — but assigning one does nothing. The value is stored, a deprecation warning says "NO LONGER CONSULTED by Spree — the override was not applied", and Spree keeps calling the workflow. A class written against the old service contract (call(order:, …) with run steps) isn't interchangeable with the workflow contract. Port it: subclass the workflow, move logic into perform/hooks, assign the new key. These names are removed in 6.1.

Legacy key (stash-only)Assign this insteadDefault
cart_add_item_servicecart_add_item_workflowSpree::Carts::AddItem
cart_recalculate_servicecart_recalculate_workflowSpree::Carts::Recalculate
cart_merge_strategycart_merge_workflowSpree::Carts::Merge
carts_complete_servicecarts_complete_workflowSpree::Carts::Complete
carts_upsert_items_servicecart_upsert_items_workflowSpree::Carts::UpsertItems
order_complete_serviceorder_complete_workflowSpree::Orders::Complete
order_cancel_serviceorder_cancel_workflowSpree::Orders::Cancel
fulfillment_create_servicefulfillment_create_workflowSpree::Fulfillments::Create
shipment_update_service, fulfillment_update_servicefulfillment_update_workflowSpree::Fulfillments::Update
payments_handle_webhook_servicepayments_handle_webhook_workflowSpree::Payments::HandleWebhook
gift_card_apply/remove/redeem_servicegift_card_apply/remove/redeem_workflowSpree::GiftCards::*

Pure renames (applied, with a warning): checkout_add_store_credit_service → store_credit_apply_service, checkout_remove_store_credit_service → store_credit_remove_service; API stock_item_serializer → stock_level_serializer, digital_serializer → digital_asset_serializer (and admin_ twins).

Removed keys (assigning raises NoMethodError at boot): carts_validate_service (completion validation is Spree::Checkout::Requirements + carts.complete.validate), checkout_next_service, checkout_update_service, checkout_complete_service, checkout_select_shipping_method_service, checkout_get_shipping_rates_service, cart_create_service, cart_update_service, cart_estimate_shipping_rates_service, cart_change_currency_service, line_item_*_service, shipment_*_service, account_*_service, and the legacy finders/sorters (products_finder, taxon_finder, variant_finder, …). Carts are created/updated via carts_create_service / carts_update_service; checkout progression is checkout_advance_service. Coming from 5.x? See spree-upgrade-5-to-6.

API serializers

API dependencies are serializers only — there are no per-API service keys; every endpoint calls the core Spree.<key> seams directly.

# server/app/serializers/my_store/product_serializer.rb
module MyStore
  class ProductSerializer < Spree::Api::V3::ProductSerializer   # Alba resource
    typelize brand_name: [:string, nullable: true]
    attribute :brand_name do |product|
      product.brand&.name
    end
  end
end

# config/initializers/spree.rb
Spree.api.product_serializer       = 'MyStore::ProductSerializer'        # Store API
Spree.api.admin_product_serializer = 'MyStore::Admin::ProductSerializer' # Admin API — separate key
  • Store and Admin keys are independent. Spree::Api::V3::Admin::ProductSerializer inherits the shipped Spree::Api::V3::ProductSerializer by constant, so swapping product_serializer does not change Admin output — subclass and swap both if you need the field in both.
  • Nested associations often resolve through the accessor (one :product, resource: proc { Spree.api.product_serializer }), so your class is used there too.
  • Adding typed fields for @spree/sdk types: typelize (see spree-typescript-sdk).

Per-controller overrides

To change one endpoint only, decorate the API controller's named hook (serializer_class, scope, collection_includes, model_class, resource_permitted_attributes):

# server/app/controllers/spree/api/v3/store/products_controller_decorator.rb
module Spree::Api::V3::Store
  module ProductsControllerDecorator
    protected

    def serializer_class
      MyStore::DetailedProductSerializer
    end
  end
end

Spree::Api::V3::Store::ProductsController.prepend(Spree::Api::V3::Store::ProductsControllerDecorator)

There are no per-controller *_service hooks — controllers call the global seams. See spree-decorators.

Using seams from your own code

Spree.cart_add_item_workflow.call(cart: cart, variant: variant, quantity: 1)   # resolves overrides
Spree.api.cart_serializer.new(cart, params: { store: current_store }).to_h

Spree::Carts::AddItem.call(...)   # ❌ bypasses any override

Extensions especially must go through the accessor so they compose with the host app's overrides.

Syntax: Spree.<key> = MyClass (recommended — fails immediately on a typo'd constant), Spree.dependencies { |d| d.<key> = MyClass } for several, or the legacy Spree::Dependencies.<key> = 'MyClass' (string; resolved lazily, still supported).

Inspecting overrides

spree rake spree:dependencies:list        # every seam with its current value, [OVERRIDDEN] marked
spree rake spree:dependencies:list | grep workflow
spree rake spree:dependencies:overrides   # only overrides: default -> current (file:line that set it)
spree rake spree:dependencies:validate    # constantize every seam; exits 1 on NameError — good CI check

overrides is the first thing to run on an inherited project. Programmatic equivalents: Spree::Dependencies.current_values, .overridden?(:cart_add_item_workflow), .override_info(:cart_add_item_workflow), .validate!; Spree::Api::Dependencies for the API side. Legacy stash-only writes don't appear as overrides — grep initializers for _service = after upgrading.

Gotchas

  • Load order: overrides go in config/initializers/spree.rb. If two gems set the same key, the last assignment wins; check with spree:dependencies:overrides.
  • Swapped workflow lost its hooks → missing workflow_key (see above).
  • Override "does nothing" after upgrade → you're assigning a legacy *_service key; look for the "NO LONGER CONSULTED" deprecation in the boot log.
  • Dropping behavior: a replacement that skips super must re-implement stock reservations, pricing providers, availability checks, recalculation and events. Read the parent's perform in full first.
  • ArgumentError at call time → your perform signature doesn't accept a keyword callers pass. Accept **rest and forward it.

Where to read further

  • node_modules/@spree/docs/dist/developer/customization/dependencies.md (https://spreecommerce.org/docs/developer/customization/dependencies) — note: parts of the published page still show API v2 storefront_* keys; the source constants are authoritative.
  • node_modules/@spree/docs/dist/developer/customization/workflows.md — hooks, the preferred alternative
  • Source: spree_core lib/spree/core/dependencies.rb (INJECTION_POINTS_WITH_DEFAULTS, LEGACY_WORKFLOW_KEYS, LEGACY_SERVICE_KEYS, RENAMED_SERVICE_KEYS), spree_api lib/spree/api/dependencies.rb, lib/tasks/dependencies.rake
  • Related skills: spree-workflows, spree-customization, spree-decorators, spree-extensions

Comments

Markdown supported