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_lanes_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.48%. Comparing base (57e8ef9) to head (ff58cdd).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6379      +/-   ##
==========================================
+ Coverage   89.46%   89.48%   +0.02%     
==========================================
  Files         977      980       +3     
  Lines       20386    20427      +41     
==========================================
+ Hits        18238    18279      +41     
  Misses       2148     2148              

☔ 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 3 times, most recently 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
@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.

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

@mamhoff mamhoff force-pushed the add-discounts-by-lane branch 2 times, most recently from e43adaf to 6a789c8 Compare December 2, 2025 10:33
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.

Nice. Some suggestions by @manleyac and me

@tvdeyen tvdeyen added this to the 4.7 milestone Dec 2, 2025
This singleton records the currently calculated promotion lane. It has
a helper to give us all the lanes before the current lane (or all the
lanes if there is no current lane).

We overwrite the setter for the lane so that the lane is always a
string.
This will replace `#discountable_amount`, and operates on in-memory
adjustments.
This is the same code as in Spree::LineItem. We'll move it to a module
soon.
Again, this has some code duplication that we'll get rid of in the next
commits.
Promotable#discounted_amount is implemented in exactly the same way for
all promotables we have: Line items, shipments, and shipping rates.
Let's move it to a module.
Line items and shipments use adjustments to record discounts, and their
`#discounts_by_lanes` method is identical. Let's DRY this up and extract
the method into a module.
When we refactor the promotion system to use in-memory
adjustments/discounts to store discounts, we need to be able to ask
which discounts are in the current promotion lane. This allows that.
@mamhoff mamhoff force-pushed the add-discounts-by-lane branch from 6a789c8 to ff58cdd Compare December 2, 2025 18:57
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

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants