Skip to content

Conversation

@mamhoff
Copy link
Contributor

@mamhoff mamhoff commented Nov 2, 2025

Summary

This changes the DSL for defining promotion conditions. I know we don't love changing APIs, but hear me out - it becomes simpler and way more straightforward, and the refactor needed is tiny. Also, together with #6357 , we can remove three smelly modules.

The API changes from (this is a real-world example):

class SecondaryShipment < SolidusPromotions::Condition
  include SolidusPromotions::Conditions::ShipmentLevelCondition

  def applicable?(promotable)
    promotable.is_a?(Spree::Shipment) || promotable.is_a?(Spree::ShippingRate)
  end

  def eligible?(promotable)
    send(:"#{promotable.class.name.demodulize.underscore}_discountable?", promotable)
  end

  private

  def shipment_discountable?(shipment)
    shipment.split_shipment_secondary?
  end

  def shipping_rate_discountable?(shipping_rate)
    shipment_discountable?(shipping_rate.shipment)
  end
end

To:

class SecondaryShipment < SolidusPromotions::Condition
  def shipment_eligible?(shipment, _options = {})
    shipment.split_shipment_secondary?
  end

  def shipping_rate_eligible?(shipping_rate, _options = {})
    shipment_discountable?(shipping_rate.shipment)
  end
end

The new API is more concise, and allows us easily to adapt conditions to the upcoming strikethrough prices mechanism, in which we need to discount Spree::Price objects (and also check eligibility of these objects). For checking the eligibility of a price, we need to take into account the current order and the desired quantity of the price's variant, so that can be accomplished with this API - without having to redefine applicable? for a whole lot of conditions.

This is what a strikethrough-price compatible condition would look like:

class AtLeastThreeWithOrderOverHundred < SolidusPromotions::Condition
  def order_eligible?(order, _options = {})
    order.total > 100
  end
  
  # eligibility of prices needs to check the current order, too. 
  def price_eligible?(price, options = {})
    order = options[:order]
    quantity = options[:quantity]
    price.variant.product == preferred_product && quantity > 3 && order_eligible(order)
  end

  # Order eligibility is checked before line item eligibility, so we don't need to check it here
  def line_item_eligible?(line_item, _options = {})
     line_item.variant.product == preferred_product && line_item.quantity > 3
  end
end

Checklist

Check out our PR guidelines for more details.

The following are mandatory for all PRs:

The following are not always needed:

  • 📖 I have updated the README to account for my changes.
  • 📑 I have documented new code with YARD.
  • 🛣️ I have opened a PR to update the guides.
  • ✅ I have added automated tests to cover my changes.
  • 📸 I have attached screenshots to demo visual changes.

@mamhoff mamhoff requested a review from a team as a code owner November 2, 2025 22:40
@github-actions github-actions bot added the changelog:solidus_promotions Changes to the solidus_promotions gem label Nov 2, 2025
@codecov
Copy link

codecov bot commented Nov 2, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.36%. Comparing base (e4ee677) to head (e21a13f).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6360      +/-   ##
==========================================
+ Coverage   89.35%   89.36%   +0.01%     
==========================================
  Files         961      961              
  Lines       20173    20193      +20     
==========================================
+ Hits        18026    18046      +20     
  Misses       2147     2147              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@tvdeyen tvdeyen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this API. Much better and more flexible for future conditions.

Since this is compatible and adds a bunch of useful deprecation warnings, I think this is fine to be shipped with next minor.

@tvdeyen tvdeyen added this to the 4.7 milestone Nov 3, 2025
@mamhoff mamhoff force-pushed the dynamic-applicable-eligible-condition-dispatch branch from 401122a to 017b2f8 Compare November 3, 2025 15:59
Prior to this, we had a bit of cumbersome way of definining a condition.
We first had to define whether the condition is applicable by
implementing a method, and then we had to define the actual,
polymorphic, eligibility check.

This commit changes the matter such that a condition can simply
implement

```rb
def order_eligible? = order.foo?
```

and the promotion system knows that this condition is applicable for
order checks. Neat!

This - along with the deprecation of `Condition#level` - allows us to
also deprecate the `OrderLevelCondition`, `LineItemLevelCondition`, and
`ShipmentLevelCondition` modules that do nothing but a type check.

Next commit will have the deprecation warnings.

For the upcoming price promotions (strikethrough prices) we need to be
able to test whether a price is eligible for a promotion given a
current order and a quantity. This adds a test for that API.
The `LineItemLevelCondition`, `OrderLevelCondition`, and
`ShipmentLevelCondition` concerns are awkwardly named, and there's no
adequate thing for conditions that target shipments and shipping rates
with this system (let alone something for prices). With the previous
commit in place and solidusio#6357, we can fully deprecate these modules for
Solidus 5.

What this does is:
If a condition includes any of the modules and defines the `eligible?`
method, we emit a deprecation warning telling the implementer to stop
including the module and rename their method. That way the `respond_to?`
logic from the base class kicks in, and we can remove the awkward API.
This deprecation warning is chonky, but it tells the implementer in
detail what to do and is easy to fix.
@mamhoff mamhoff force-pushed the dynamic-applicable-eligible-condition-dispatch branch from 017b2f8 to e21a13f Compare November 4, 2025 16:14
@mamhoff mamhoff merged commit 1cc690d into solidusio:main Nov 4, 2025
39 checks passed
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 20, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.
@mamhoff mamhoff mentioned this pull request Nov 20, 2025
5 tasks
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 20, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 20, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 20, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.

Also fixes some AI slop in the docs.
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 21, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.

Also fixes some AI slop in the docs.
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 22, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.

Also fixes some AI slop in the docs.
tvdeyen pushed a commit to mamhoff/solidus that referenced this pull request Nov 24, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.

Also fixes some AI slop in the docs.
mamhoff added a commit to mamhoff/solidus that referenced this pull request Nov 27, 2025
Similar to the work in solidusio#6360, this improves the DSL for creating
benefits. Rather than having to define one method that cares for
applicability to a discountable, we now simply define that a benefit
`#can_discount?` an object if its public interface has
`#discount_{discountable_type}` defined.

This does not change the implementation of the different methods, but
that will come when we refactor the promotion system to use a single
system of record for discounts. Then, different objects will need
different discount records (line items and shipment use adjustments, but
shipping rates use shipping rate discounts etc.).

I'm not doing the work for adding deprecation warnings here, as this
code was never meant to be overridden up to now.

Also fixes some AI slop in the docs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:solidus_promotions Changes to the solidus_promotions gem

Projects

Development

Successfully merging this pull request may close these issues.

3 participants