Skip to content

IntroEligibilityCalculator: fixed logic for subscriptions in same group#2174

Merged
NachoSoto merged 15 commits into
mainfrom
trial-checker-intergration-test-sub-group
Jan 25, 2023
Merged

IntroEligibilityCalculator: fixed logic for subscriptions in same group#2174
NachoSoto merged 15 commits into
mainfrom
trial-checker-intergration-test-sub-group

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Dec 20, 2022

Copy link
Copy Markdown
Contributor
  • Fixes TRIAGE-221 for SK1.
  • Filed FB11889732 for SK2.

Changes:

  • Added 2 IntroEligibilityCalculatorTests unit tests to cover this scenario
  • Added 1 IntroEligibilityCalculatorTests unit tests to cover the equivalent of the testIneligibleForIntroAfterPurchaseExpires integration test
  • Added 2 StoreKitIntegrationTests to cover this scenario (one of them disabled in SK2)
  • Updated IntroEligibilityCalculator logic to fix the new tests.
  • Refactored IntroEligibilityCalculatorTests to avoid duplication and allow adding new cases more easily.
  • Added a few more tests to ensure other cases are correct.

Comment thread Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift
@NachoSoto NachoSoto force-pushed the trial-checker-intergration-test-sub-group branch from f8561c2 to d37ac1b Compare December 21, 2022 17:13
@NachoSoto NachoSoto force-pushed the trial-checker-intergration-test-sub-group branch from d37ac1b to 94ddd67 Compare December 22, 2022 20:03
NachoSoto added a commit that referenced this pull request Dec 22, 2022
This simplifies the `+=` implementation by avoiding an intermediate dictionary, and allows a simpler way of merging dictionaries.

Will be used in #2174.
@NachoSoto NachoSoto changed the base branch from main to dictionary-merge December 22, 2022 21:47
@NachoSoto NachoSoto force-pushed the trial-checker-intergration-test-sub-group branch from 20dc327 to d10bd9f Compare December 22, 2022 21:47
@NachoSoto NachoSoto requested a review from aboedo December 22, 2022 21:47
@NachoSoto NachoSoto added pr:fix A bug fix and removed test labels Dec 22, 2022
@NachoSoto NachoSoto changed the title TrialOrIntroPriceEligibilityChecker: added integration test to verify behavior with subscription group IntroEligibilityCalculator: fixed logic for subscriptions in same group Dec 22, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplified this using the MergeStrategy (see #2190)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how does this differ from result += eligibility?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Disabled for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The time rate is the minimum, but it's not a weekly subscription, it's monthly, so it's not the "immediate expiration" bug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This test used to pass before too, but it helped me make sure I didn't break this scenario, thanks for the suggestion @aboedo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This test was failing before this fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These 2 tests were failing before the fix.

@NachoSoto

Copy link
Copy Markdown
Contributor Author

@aboedo this is ready now, I think the logic is correct?

@NachoSoto NachoSoto requested review from a team and vegaro December 22, 2022 21:59
@NachoSoto

Copy link
Copy Markdown
Contributor Author

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.

@NachoSoto NachoSoto added the WIP label Dec 22, 2022
Comment thread Sources/Purchasing/IntroEligibilityCalculator.swift Outdated
Comment thread Sources/Purchasing/IntroEligibilityCalculator.swift Outdated
Base automatically changed from dictionary-merge to main December 27, 2022 17:47
@NachoSoto NachoSoto force-pushed the trial-checker-intergration-test-sub-group branch from 1706286 to a6257ea Compare January 12, 2023 18:24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how does this differ from result += eligibility?

Comment on lines +118 to +121
let expiredSubscriptionForProduct = expiredSubscriptionProducts
.lazy
.map(\.productIdentifier)
.contains(candidate.productIdentifier)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 1 is active
  • if the developer then adds a free trial to yearly 1 and 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

all in all, this means that this check isn't needed:
check is

  • !activeSubscriptionInGroup and !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.:
    !activeSubscriptionForProductOrSubsGroup and !expiredTrialForProductOrSubsGroup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
        ]
    )
}

@NachoSoto NachoSoto force-pushed the trial-checker-intergration-test-sub-group branch from 83373ae to ae5fa63 Compare January 17, 2023 21:13
@NachoSoto NachoSoto requested a review from aboedo January 17, 2023 21:15
@NachoSoto

Copy link
Copy Markdown
Contributor Author

@aboedo third time is the charm hopefully! Let me know if this is good now 🙏🏻

@NachoSoto NachoSoto force-pushed the trial-checker-intergration-test-sub-group branch from ae5fa63 to 7d91eda Compare January 21, 2023 16:46
@NachoSoto NachoSoto requested a review from a team January 21, 2023 16:49
@codecov

codecov Bot commented Jan 21, 2023

Copy link
Copy Markdown

Codecov Report

Merging #2174 (7d91eda) into main (4533538) will increase coverage by 0.04%.
The diff coverage is 100.00%.

@@            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     
Impacted Files Coverage Δ
.../LocalReceiptParsing/BasicTypes/AppleReceipt.swift 100.00% <100.00%> (ø)
...LocalReceiptParsing/BasicTypes/InAppPurchase.swift 100.00% <100.00%> (ø)
...ources/Purchasing/IntroEligibilityCalculator.swift 100.00% <100.00%> (ø)
Sources/Logging/Strings/StoreKitStrings.swift 91.17% <0.00%> (+1.47%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@aboedo aboedo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +105 to +114
let activeSubscriptionInGroup = (
candidate.subscriptionGroupIdentifier != nil &&
activeSubscriptionsProducts.contains {
$0.subscriptionGroupIdentifier == candidate.subscriptionGroupIdentifier
}
)
let expiredTrialInGroup = (
candidate.subscriptionGroupIdentifier != nil &&
expiredTrialProducts.contains {
$0.subscriptionGroupIdentifier == candidate.subscriptionGroupIdentifier

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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: ineligible if product1 is active subscription or expired but had trial, but this would return unknown

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

func testIneligibleForIntroForDifferentProductInSameSubscriptionGroupAfterPurchase() async throws {
try XCTSkipIf(
Self.storeKit2Setting == .enabledForCompatibleDevices,
"This test currently does not pass with SK2 (see FB11889732)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this one under the company account? I can't find it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is. Click on "Inbox" instead of "Submitted" because it got a response so it's there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!! That's exactly what happened 🤦 I was only checking "submitted"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

for some reason I never realized that you can do this syntax (i.e.: customerInfo = try await purchase().customerinfo. That's awesome

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I love these async tests :D

@NachoSoto NachoSoto merged commit fdd7874 into main Jan 25, 2023
@NachoSoto NachoSoto deleted the trial-checker-intergration-test-sub-group branch January 25, 2023 16:40
NachoSoto added a commit that referenced this pull request Jan 25, 2023
NachoSoto added a commit that referenced this pull request Feb 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants