IntroEligibilityCalculator: fixed logic for subscriptions in same group#2174
Conversation
f8561c2 to
d37ac1b
Compare
d37ac1b to
94ddd67
Compare
This simplifies the `+=` implementation by avoiding an intermediate dictionary, and allows a simpler way of merging dictionaries. Will be used in #2174.
20dc327 to
d10bd9f
Compare
TrialOrIntroPriceEligibilityChecker: added integration test to verify behavior with subscription groupIntroEligibilityCalculator: fixed logic for subscriptions in same group
There was a problem hiding this comment.
Simplified this using the MergeStrategy (see #2190)
There was a problem hiding this comment.
how does this differ from result += eligibility?
There was a problem hiding this comment.
Oh it's the same, I'll change it, thanks!
I improved this from the previous implementation after bae4a00 but didn't realize it can be even simpler now.
There was a problem hiding this comment.
Disabled for now.
There was a problem hiding this comment.
it's crazy that this fails. this is with timeRate > the minimum? just wondering if it's a bug related to the thing where subs would immediately expire
There was a problem hiding this comment.
The time rate is the minimum, but it's not a weekly subscription, it's monthly, so it's not the "immediate expiration" bug.
There was a problem hiding this comment.
This test used to pass before too, but it helped me make sure I didn't break this scenario, thanks for the suggestion @aboedo.
There was a problem hiding this comment.
This test was failing before this fix.
There was a problem hiding this comment.
These 2 tests were failing before the fix.
|
@aboedo this is ready now, I think the logic is correct? |
|
Thanks to all our test coverage I realized that the new logic wasn't quite right. I've added another failing unit test, but now I have to figure out how to reconcile both scenarios. |
1706286 to
a6257ea
Compare
There was a problem hiding this comment.
how does this differ from result += eligibility?
| let expiredSubscriptionForProduct = expiredSubscriptionProducts | ||
| .lazy | ||
| .map(\.productIdentifier) | ||
| .contains(candidate.productIdentifier) |
There was a problem hiding this comment.
We could potentially have a false negative here:
Expired subs for the product only matter if you have used a free trial for the product.
For example, if you subscribe to yearly 1, and it has no free trial available:
- you're not eligible while the subscription to
yearly 1is active - if the developer then adds a free trial to
yearly 1and you cancel the subscription, you're eligible because you haven't used a free trial for this product (or its group), and one is available
There was a problem hiding this comment.
all in all, this means that this check isn't needed:
check is
!activeSubscriptionInGroupand!expiredTrialInGroup.
There isn't anything specific to the product itself, but... if arguably the product didn't have a subs group, the same would apply to it. In practice the field is nullable but I don't think you can have products without subs groups anymore, but it can't hurt to still check for it, i.e.:
!activeSubscriptionForProductOrSubsGroupand!expiredTrialForProductOrSubsGroup.
There was a problem hiding this comment.
Thanks a lot for the thorough review!
I think my TDD approach has kinda failed me here. I’ve been writing tests and finding the correct implementation that satisfies all tests, but it led me to the wrong implementation.
I misinterpreted the is_in_trial_period property in the receipt, I figured that wouldn't be true for an expired subscription, but now I see that it is (and verified through integration tests).
So your suggested implementation works after I fix the incorrect unit test 👍🏻
I've also add a new unit test to cover this false positive you described, which failed before the fix:
func testCheckTrialOrIntroDiscountEligibilityReturnsEligibleForPreviouslyOwnedSubscriptionWithUnusedTrial() {
self.testEligibility(
purchaseExpirationsByProductIdentifier: [
("com.revenuecat.product1", Date().addingTimeInterval(-1000), false)
],
productsInGroups: [
"com.revenuecat.product1": (groupID: "group1", hasTrial: true)
],
expectedResult: [
"com.revenuecat.product1": .eligible
]
)
}83373ae to
ae5fa63
Compare
|
@aboedo third time is the charm hopefully! Let me know if this is good now 🙏🏻 |
…fy behavior with subscription group See [TRIAGE-221].
ae5fa63 to
7d91eda
Compare
Codecov Report
@@ Coverage Diff @@
## main #2174 +/- ##
==========================================
+ Coverage 85.79% 85.84% +0.04%
==========================================
Files 183 183
Lines 12077 12105 +28
==========================================
+ Hits 10362 10391 +29
+ Misses 1715 1714 -1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
aboedo
left a comment
There was a problem hiding this comment.
Looks great!
I left a comment about an edge case that I'm not sure is possible to hit anymore, but it'd be nice to still consider.
| let activeSubscriptionInGroup = ( | ||
| candidate.subscriptionGroupIdentifier != nil && | ||
| activeSubscriptionsProducts.contains { | ||
| $0.subscriptionGroupIdentifier == candidate.subscriptionGroupIdentifier | ||
| } | ||
| ) | ||
| let expiredTrialInGroup = ( | ||
| candidate.subscriptionGroupIdentifier != nil && | ||
| expiredTrialProducts.contains { | ||
| $0.subscriptionGroupIdentifier == candidate.subscriptionGroupIdentifier |
There was a problem hiding this comment.
this would miss eligibility for a product that doesn't have a subscription. In practice I'm not sure it's possible to have one anymore, but it'd be nice to cover for it.
i.e.:
- purchase
product1,subsGroupIdentifier = nil - eligibility should be:
ineligibleifproduct1is active subscription or expired but had trial, but this would returnunknown
There was a problem hiding this comment.
We're talking about this offline. I'll merge this for now since it's over a month old and it already fixes the main issue.
I'll that those 2 new tests in a separate PR.
| func testIneligibleForIntroForDifferentProductInSameSubscriptionGroupAfterPurchase() async throws { | ||
| try XCTSkipIf( | ||
| Self.storeKit2Setting == .enabledForCompatibleDevices, | ||
| "This test currently does not pass with SK2 (see FB11889732)" |
There was a problem hiding this comment.
is this one under the company account? I can't find it
There was a problem hiding this comment.
It is. Click on "Inbox" instead of "Submitted" because it got a response so it's there.
There was a problem hiding this comment.
Thanks!! That's exactly what happened 🤦 I was only checking "submitted"
There was a problem hiding this comment.
it's crazy that this fails. this is with timeRate > the minimum? just wondering if it's a bug related to the thing where subs would immediately expire
| let productWithNoTrial = try await self.product(Self.group3MonthlyNoTrialProductID) | ||
| let productWithTrial = try await self.annualPackage.storeProduct | ||
|
|
||
| let customerInfo = try await Purchases.shared.purchase(product: productWithNoTrial).customerInfo |
There was a problem hiding this comment.
for some reason I never realized that you can do this syntax (i.e.: customerInfo = try await purchase().customerinfo. That's awesome
There was a problem hiding this comment.
Yeah I love these async tests :D
…o subscription group Follow up to #2174.

SK1.FB11889732forSK2.Changes:
IntroEligibilityCalculatorTestsunit tests to cover this scenarioIntroEligibilityCalculatorTestsunit tests to cover the equivalent of thetestIneligibleForIntroAfterPurchaseExpiresintegration testStoreKitIntegrationTeststo cover this scenario (one of them disabled in SK2)IntroEligibilityCalculatorlogic to fix the new tests.IntroEligibilityCalculatorTeststo avoid duplication and allow adding new cases more easily.