Skip to content

Conversation

@yashranaway
Copy link
Contributor

Issue: #1410

Description

Problem

When an offer code hits its max usage limit (or gets deleted/expires) between installment charges, subsequent payments lose the discount even though the customer legitimately used the code on their first payment.

Solution

Instead of checking the live offer code on each charge, we now copy the discount data from the original purchase's purchase_offer_code_discount record. This data was already being stored - we just weren't using it for subsequent charges.

Two changes:

  1. Subscription#build_purchase - copies cached discount data to new purchases
  2. Purchase#original_offer_code - prioritizes cached data over live offer code lookup

Test Results

image image

Added tests for:

  • Deleted offer codes (installments + memberships)
  • Expired offer codes
  • Max usage reached
  • Offer code amount changed after purchase
  • Percentage discounts
  • Backwards compatibility with legacy purchases

Checklist


AI Disclosure

Claude Opus 4.5 was used via Claude Code was used for implementation and test writing.

Comment on lines +251 to +265
if discount_applies_to_next_charge?
if original_purchase.purchase_offer_code_discount.present?
original_discount = original_purchase.purchase_offer_code_discount
purchase.offer_code = original_purchase.offer_code
purchase.build_purchase_offer_code_discount(
offer_code: original_discount.offer_code,
offer_code_amount: original_discount.offer_code_amount,
offer_code_is_percent: original_discount.offer_code_is_percent,
pre_discount_minimum_price_cents: original_discount.pre_discount_minimum_price_cents,
duration_in_months: original_discount.duration_in_months
)
elsif original_purchase.offer_code.present?
purchase.offer_code = original_purchase.offer_code
end
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the Core fix - we check if original_purchase.purchase_offer_code_discount exists and copy all fields to the new purchase. Falls back to live offer code for legacy purchases that don't have cached data.

Comment on lines -2414 to +2420
return nil if offer_code&.deleted? && !include_deleted

if has_cached_offer_code?
code = purchase_offer_code_discount.offer_code.code
code = purchase_offer_code_discount.offer_code&.code
purchase_offer_code_discount.offer_code_is_percent ?
OfferCode.new(amount_percentage: purchase_offer_code_discount.offer_code_amount, code:) :
OfferCode.new(amount_cents: purchase_offer_code_discount.offer_code_amount, code:)
elsif offer_code&.deleted? && !include_deleted
nil
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reordered the logic - check cached data first, then check if offer code is deleted. Previously we'd return nil for deleted codes before even checking if we had cached data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test directly covers the reported issue #1410 - offer code with max_purchase_count=1, verify quantity_left <= 0 after first purchase, then confirm discount still applies to next installment.

if discount_applies_to_next_charge?
if original_purchase.purchase_offer_code_discount.present?
original_discount = original_purchase.purchase_offer_code_discount
purchase.offer_code = original_purchase.offer_code
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting purchase.offer_code even when it might be nil/deleted - this is intentional for audit trail, the actual discount comes from the cached data.

@yashranaway
Copy link
Contributor Author

@EmCousin _a Hey, I have updated the logic as per previous discussion #2257 (comment). Could you please review?

@yashranaway
Copy link
Contributor Author

@EmCousin light ping on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants