Store automatically added BuyXGetY reward lines using the morph map - #2632
Merged
glennjacobs merged 2 commits intoAug 25, 2026
Merged
Conversation
Resolves the test-file conflict with lunarphp#2631 by keeping both new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
glennjacobs
approved these changes
Aug 25, 2026
This was referenced Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A reward line added automatically by a Buy X Get Y discount is written to
lunar_cart_lineswith a differentpurchasable_typethan every other lineholding the same product.
What happens now
Lunar\Models\ProductVariant, while a line theshopper added stores
product_variant.cart.
whereHasMorph, a custom pipeline stage — silently skips these lines.lunar_order_lines, so the inconsistency is persistedagainst the placed order too.
What should happen
purchasable_typeas any other line for thatpurchasable.
Why
Lunar registers a morph map at boot (
ModelManifest::morphMap(), keyed by thesnake-cased class basename), so
ProductVariant::getMorphClass()returnsproduct_variant. Every cart line in the codebase is created withgetMorphClass(), except this one:get_class()bypasses the map and writes the concrete class name.The fix
Use
getMorphClass(), matching how cart lines are created everywhere else. Italso means a store that has extended
ProductVariantgets the mapped aliasrather than its own class name written into the table.
Deliberately out of scope: with
reward_qtygreater than one this method alsocreates one cart line per reward unit rather than a single line, because its
"is it already in cart?" check reads an in-memory
$cart->linesthe loop neverappends to. That is a real but separate defect, and fixing it here would make
this diff much harder to review. Also note #2509 is currently rewriting this
method for stock availability — no overlap in intent, but a rebase for whichever
lands second.
Tests
tests/core/Unit/DiscountTypes/BuyXGetYTest.php:can store an automatically added reward line using the morph map — an
automatic reward on a qualifying cart, asserting the persisted
purchasable_type. Fails on1.xwith: