Skip to content

Conversation

ThuktenSingye
Copy link

@ThuktenSingye ThuktenSingye commented Oct 11, 2025

Summary

This pull request introduces coupon code case sensitivity support to Solidus Promotions.
It allows to configure whether promotion codes should be matched case-sensitively or case-insensitively.

By default, coupon codes remain case-insensitive, so promo code like SAVE20, save20, or Save20 will all apply the promotion successfully. With this update, you can enable a case-sensitive mode where the coupon code must match exactly (e.g., only SAVE20 is valid, while save20 or Save20 are not).

Key changes

  • Added a new configuration preference:
SolidusPromotions.config.preferred_coupon_code_case_sensitive
  • Updated promotion handlers and models to respect the case sensitivity configuration.
  • Added detailed documentation:
    • README section explaining how to enable and use this feature.
    • YARD documentation for CouponCodeCaseSensitivity and the configuration preference.
  • Clarified behavior for code normalization, lookups, and promotion application.

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 added automated tests to cover my changes.

Add a new configuration option to control the case sensitivity of
coupon code and implement across all relevant models and handlers.

Changes include:

- Create CouponCodeCaseSensitivity concern that provides both
  class and instance methods to check the case sensitivity
  preference

- Add preferred_coupon_code_case_sensitive preference to
  Configuration with a default value of false to maintain
  backward compatibility

- Include concern in PromotionCode model and update normalize_code
  method to conditionally downcase based on configuration

- Include concern in Promotion model and update with_coupon_code
  class method to perform case-sensitive or insensitive lookups

- Include concern in PromotionHandler::Coupon and update
  coupon_code initialization to handle case sensitivity

- Include concern in OrderPatch to make the case sensitivity
  behavior available to Order instances

- Update coupon_code= setter in Spree::Order to conditionally
  normalize the code based on case sensitivity configuration,
  stripping whitespace and downcasing only when case-insensitive
  mode is enabled

The default behavior remains case-insensitive to preserve existing
functionality. To enable case sensitivity via:

  SolidusPromotions.configure do |config|
    config.preferred_coupon_code_case_sensitive = true
  end

signed-off-by: Thukten Singye <thuktensingye2163@gmail.com>
Add comprehensive test coverage for the coupon code case
sensitivity feature across all affected models and handlers.

Changes include:

- Create CouponCodeCaseSensitivity concern spec to test both
  class and instance methods for checking the preference in
  case-sensitive and case-insensitive modes

- Update PromotionCode spec to verify code normalization behavior
  in both modes, including validation of duplicate codes with
  different cases when case-sensitive mode is enabled

- Update Promotion spec to verify with_coupon_code class method
  performs correct lookups in both case-sensitive and
  case-insensitive modes, and preserves original case when
  appropriate

- Update PromotionHandler::Coupon spec to verify coupon
  application succeeds with exact case match and fails with
  incorrect case when case-sensitive mode is enabled

- Update Spree::Order spec to verify coupon_code setter correctly
  normalizes codes based on configuration, downcasing only when
  case-insensitive mode is active

Tests cover both default behavior (case-insensitive) and
case-sensitive mode to ensure backward compatibility and correct
implementation of the new feature.

signed-off-by: Thukten Singye <thuktensingye2163@gmail.com>
Add comprehensive documentation describing the coupon code case
sensitivity configuration and its impact on promotion behavior.

- Added YARD documentation for the configuration preference explaining
  the difference between case-sensitive and case-insensitive modes.

- Added detailed module documentation for `CouponCodeCaseSensitivity`.

- Updated the promotions README with a new section describing the
  feature.

signed-off-by: Thukten Singye <thuktensingye2163@gmail.com>
@ThuktenSingye ThuktenSingye requested a review from a team as a code owner October 11, 2025 18:17
@github-actions github-actions bot added changelog:solidus_core Changes to the solidus_core gem changelog:solidus_promotions Changes to the solidus_promotions gem labels Oct 11, 2025
@kennyadsl kennyadsl requested a review from Copilot October 13, 2025 07:56
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request introduces configurable case sensitivity for coupon codes in Solidus Promotions. By default, coupon codes remain case-insensitive (maintaining backward compatibility), but administrators can now enable strict case-sensitive matching where "SAVE20" and "save20" are treated as distinct codes.

  • Added a new configuration preference preferred_coupon_code_case_sensitive (defaults to false)
  • Implemented case sensitivity logic across promotion models, handlers, and order processing
  • Added comprehensive test coverage for both case-sensitive and case-insensitive scenarios

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
promotions/lib/solidus_promotions/configuration.rb Adds the new boolean configuration preference with detailed documentation
promotions/app/models/concerns/solidus_promotions/coupon_code_case_sensitivity.rb New concern providing shared methods for checking case sensitivity configuration
promotions/app/models/solidus_promotions/promotion.rb Updates coupon code lookup to respect case sensitivity setting
promotions/app/models/solidus_promotions/promotion_code.rb Modifies code normalization to preserve case when sensitivity is enabled
promotions/app/models/solidus_promotions/promotion_handler/coupon.rb Updates coupon handler to conditionally downcase codes based on configuration
promotions/app/patches/models/solidus_promotions/order_patch.rb Includes case sensitivity concern in order model
core/app/models/spree/order.rb Updates coupon code assignment to respect case sensitivity
promotions/README.md Adds comprehensive documentation explaining the feature and its implications
Multiple test files Extensive test coverage for case-sensitive and case-insensitive scenarios

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

end
end

context "when preferred_coupon_case_code_sensitive is true" do
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

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

Corrected spelling of 'preferred_coupon_case_code_sensitive' to 'preferred_coupon_code_case_sensitive'.

Suggested change
context "when preferred_coupon_case_code_sensitive is true" do
context "when preferred_coupon_code_case_sensitive is true" do

Copilot uses AI. Check for mistakes.

context "with extra spacing" do
let(:code) { " new code " }

it "remove surrounding whitespace" do
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

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

Corrected grammar from 'remove' to 'removes'.

Suggested change
it "remove surrounding whitespace" do
it "removes surrounding whitespace" do

Copilot uses AI. Check for mistakes.

@kennyadsl
Copy link
Member

@ThuktenSingye Thanks! Copilot has found some small issues that I think we can easily fix.

Overall, the change seems very good to me, but I'd like to get @mamhoff opinion as well, in case I'm missing something.

@ThuktenSingye
Copy link
Author

@kennyadsl Thank you for the feedback. I’ll be glad to incorporate any additional improvements or refactors.

@mamhoff
Copy link
Contributor

mamhoff commented Oct 13, 2025

I'd love to know the business case for this - usually, as a business I want customers to be able to successfully use my promotion codes, even if they e.g. only heard them on the radio or on a podcast. For the English language, this is a feature that's useful for very, very few stores, and setting it to true might seem like a good idea until the complaints come piling in. I'd rather not have it in the default configuration.

Can we make the coupon code normalization a configurable class instead?

I'm thinking of

# promotions/configuration.rb
class_name_attribute :coupon_code_normalizer_class, default: "Promotions::CaseInsensitiveCode"

# This is the class and its API 
module SolidusPromotions
  class CaseInsensitiveCode
    def self.call(value)
      value.strip.downcase
    end
  end
end

# This is how it would be used, e.g. in `SolidusPromotions::PromotionCode
def normalize_code
  self.value = SolidusPromotions.config.coupon_code_normalizer_class.call(value)
end

This way, y'all are free to implement a CaseSensitiveCode class to your own app, and we don't have to maintain the feature which I see of questionable use.

@ThuktenSingye
Copy link
Author

I'd love to know the business case for this - usually, as a business I want customers to be able to successfully use my promotion codes, even if they e.g. only heard them on the radio or on a podcast. For the English language, this is a feature that's useful for very, very few stores, and setting it to true might seem like a good idea until the complaints come piling in. I'd rather not have it in the default configuration.

Can we make the coupon code normalization a configurable class instead?

I'm thinking of

# promotions/configuration.rb
class_name_attribute :coupon_code_normalizer_class, default: "Promotions::CaseInsensitiveCode"

# This is the class and its API 
module SolidusPromotions
  class CaseInsensitiveCode
    def self.call(value)
      value.strip.downcase
    end
  end
end

# This is how it would be used, e.g. in `SolidusPromotions::PromotionCode
def normalize_code
  self.value = SolidusPromotions.config.coupon_code_normalizer_class.call(value)
end

This way, y'all are free to implement a CaseSensitiveCode class to your own app, and we don't have to maintain the feature which I see of questionable use.

I'd love to know the business case for this - usually, as a business I want customers to be able to successfully use my promotion codes, even if they e.g. only heard them on the radio or on a podcast. For the English language, this is a feature that's useful for very, very few stores, and setting it to true might seem like a good idea until the complaints come piling in. I'd rather not have it in the default configuration.

Can we make the coupon code normalization a configurable class instead?

I'm thinking of

# promotions/configuration.rb
class_name_attribute :coupon_code_normalizer_class, default: "Promotions::CaseInsensitiveCode"

# This is the class and its API 
module SolidusPromotions
  class CaseInsensitiveCode
    def self.call(value)
      value.strip.downcase
    end
  end
end

# This is how it would be used, e.g. in `SolidusPromotions::PromotionCode
def normalize_code
  self.value = SolidusPromotions.config.coupon_code_normalizer_class.call(value)
end

This way, y'all are free to implement a CaseSensitiveCode class to your own app, and we don't have to maintain the feature which I see of questionable use.

@mamhoff Thank you for the feedback!. I agree that case-insensitive is the better default for customer experience, which is exactly why I implemented it that way. By default, the behavior is unchanged - codes remain case-insensitive just as Solidus has always maintained.

The preference is opt-in, so developers have to explicitly enable case-sensitivity. Users won't encounter this unless a developer intentionally changes the configuration.

With this boolean preference approach, users who need case-sensitive codes simply add one line to their configuration:

config.preferred_coupon_code_case_sensitive = true

They don't need to create any custom classes and make changes in multiple file. It just seems more straightforward and easy for developers.

As you mention, as a business, it depends. some business prefer this way and some might not though it will made easier for those who want.

@mamhoff
Copy link
Contributor

mamhoff commented Oct 13, 2025

Your reply does not include a business case, and it misses my point. I think it's preferable to not make this too easy, and we have a well-established pattern in the Solidus community to add custom classes to change behavior.

I'm not going to die on this hill. Core team: You decide. :)

@kennyadsl
Copy link
Member

The @mamhoff's point is valid. There's no need to add other possible implementations of the interface, as long as it's easily configurable. Martin is suggesting to keep the class configurable, and you swap it with your own in your Solidus app.

The point is also that we are a small team, and we are not able to maintain all the possible ramifications of the use cases. Still, Solidus can be flexible enough to let you customize this part.

If you can make this change, this would be the preferred way of handling this change for me as well.

@ThuktenSingye
Copy link
Author

@mamhoff and @kennyadsl Thank you for the feedback! I was elaborating how my current approach wouldn't affect the default Solidus behavior though it was off from the solidus pattern. The custom configurable class approach makes sense for keeping the core maintainable while still providing flexibility.

I encountered this need working with a client who runs events and they distribute the promotional code exclusively to their subscriber base or VIP customers. It just prevent from unauthorized redemptions and prevent excessive use of coupon code. There might be other business cases which I have no idea but I am happy to update and contribute if you find this needed down the road.

Thank you for you time! :)

@kennyadsl
Copy link
Member

@ThuktenSingye Thanks to you! Looking forward to the updated PR!

@mamhoff
Copy link
Contributor

mamhoff commented Oct 13, 2025 via email

@ThuktenSingye ThuktenSingye marked this pull request as draft October 13, 2025 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:solidus_core Changes to the solidus_core gem changelog:solidus_promotions Changes to the solidus_promotions gem

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants