Skip to content

Conversation

@mamhoff
Copy link
Contributor

@mamhoff mamhoff commented Nov 21, 2025

Summary

This adds the DiscountedAmount module to the promotion system. It is preparatory work to be able to calculate the discounted_amount for each discountable item depending on the current promotion lane, without taking into account adjustments/discounts from later lanes or discounts that are market for destruction.

This just adds methods, their use will be done in a subsequent PR.

Extracted from #6371 .

This introduces an Object to hold the current promotion lane while calculating, and adds a couple of methods in order to replace a couple of older methods.

New: discountable.discounted_amount. Will replace discountable.discountable_amount.
Rationale: When called from within the promotion system, these mean the same thing (we can only discount from what is still available to discount, and if there's previous discounts, the discountable amount is equal to the discounted amount). Also: discounted_amount is calculated from real adjustments, so it can be called from outside the promotion system, and the return value will make sense (it will be equal to total_before_tax unless someone adds manual adjustments somewhere. That's why this should stay public.

New: discountable.previous_lane_discounts. Will replace discountable.current_discounts
Rationale: We need a different name for this as it returns an array of adjustments, rather than an array of SolidusPromotion::ItemDiscount objects. I could make this private, too, as it's only used from discounted_amount above.

New: discountable.current_lane_discounts. Replaces a local variable in the order discounter.
Rationale: We need to know which discounts are being currently added in the current lane so that we can mark those for destruction that we don't want. Here's where it will be used (draft PR): ea4cd30#diff-828a39d27bc67194ff2509c675bf0148420849b1fcafabe848605f4e2d81f085R69.

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 21, 2025 09:19
@github-actions github-actions bot added the changelog:solidus_promotions Changes to the solidus_promotions gem label Nov 21, 2025
@codecov
Copy link

codecov bot commented Nov 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.47%. Comparing base (fc3adf7) to head (87223f1).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6379      +/-   ##
==========================================
+ Coverage   89.45%   89.47%   +0.01%     
==========================================
  Files         974      978       +4     
  Lines       20322    20357      +35     
==========================================
+ Hits        18179    18214      +35     
  Misses       2143     2143              

☔ 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.

@mamhoff mamhoff force-pushed the add-discounts-by-lane branch 2 times, most recently from 8084c84 to a101831 Compare November 21, 2025 09:59
This allows setting the current promotion lane to the current thread,
and run computations with the current promotion lane set.

This is useful for calculating what the discounted amount of a
promotable is within e.g. the `default` lane even if there are
adjustments for the `post` lane.

I'm using `Thread.current` here because we don't always have an order to
store the current lane on.
@mamhoff mamhoff force-pushed the add-discounts-by-lane branch from a101831 to 3be73ac Compare November 22, 2025 16:28
@mamhoff mamhoff changed the title Add methods to get discounts by lane Add #discounted_amount by adjustments Nov 22, 2025
This will return the lanes that came before the current lane. Useful
for finding the discountable amount by lane.

If no lane is present, we just return all lanes, as the expectation is
that any adjustments that are present and promotion adjustments would be
relevant as we're outside the promotion calculation process.
This can be used to select all adjustments of an adjustable - a line
item or shipment - that are associated to a promotion benefit by
lanes. This is useful for getting the current discountable amount
programmatically, as will be seen in upcoming commits.
This can be used to calculate the current discountable amount while
calculating promotions.
This is more accurate than the current `discountable_amount`: It
described the amount the item has already been adjusted by, according to
the current promotion lane, and what the item has been adjusted by due
to discounts from the promotion system if called from outside the
promotion system.

It's very similar to `total_before_tax`, but excludes any manual
adjustments.
If we mark any discounts for destruction, they should not be part of the
total.
@mamhoff mamhoff force-pushed the add-discounts-by-lane branch from 3be73ac to 536b0ce Compare November 23, 2025 00:25
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.

This is very clever code, but honestly I am not very happy with this approach.

I would like to not mix in even more modules into already large objects (LineItem, Shipment, etc.) just for storing an in memory calculation.

That we now have a raise condition in a module so we are sure that we never call that public methods outside of the promotion run is a smell imo.

Maybe we can use some kind of object that holds the current state during calculation instead of the objects itself?

Would like to have some thoughts from other @solidusio/core-team members as well .

@mamhoff
Copy link
Contributor Author

mamhoff commented Nov 24, 2025

I would like to not mix in even more modules into already large objects (LineItem, Shipment, etc.) just for storing an in memory calculation.

That we now have a raise condition in a module so we are sure that we never call that public methods outside of the promotion run is a smell imo.

I've tried - hard, over many months - to use a specialized order object just for promotion calculation. However, in order to discount an order, it needs to be mutated, and I have not found a way to expose all of the order to calculators and conditions (because that's what the calling code expects) from an object that is another class than Spree::Order. I've tried inheriting from SimpleDelegator, I've even tried Refinements. ActiveRecord and Rails are not suited to that kind of programming, as much as I'd like them to be.

The goal of this work is to actually go closer to the real order object. To use adjustments/shipping rate discounts to store discounts, rather than the double bookkeeping we have now with discounts while calculating promotions, and adjustments outside of it.

A side requirement I have is to require as little change to conditions and calculators as possible, as many stores have many custom conditions and calculators.

We don't need to re-implement ActiveSupport::CurrentAttributes.

Also, CurrentAttributes implements exactly what we need.
@mamhoff mamhoff force-pushed the add-discounts-by-lane branch from 00fd257 to ac0f616 Compare November 24, 2025 20:54
@mamhoff
Copy link
Contributor Author

mamhoff commented Nov 24, 2025

I would like to not mix in even more modules into already large objects (LineItem, Shipment, etc.) just for storing an in memory calculation.

This work allows us to remove the SolidusPromotions::DiscountableAmount module from line item, shipment, and shipping rate, and with it four public methods. DiscountedAmount only needs two private methods (mostly because we don't need to add an in-memory discounts collection, and we don't need to manage these discounts from the outside).

The idea in #6371 is to store the results of all calculations on objects that we will ultimately store in the database.

Since there's concern about the method surface of Shipment, Shipping
Rate and Line Item, I'll move `previous_lane_discounts` to the private
section.
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

None yet

Development

Successfully merging this pull request may close these issues.

2 participants