Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: rewrite stripe bug in unsubscribing #2084

Merged
merged 3 commits into from
Feb 20, 2024

Conversation

ShubhamPalriwala
Copy link
Contributor

What does this PR do?

  • Fixes bug where the onConfirm & onDiscard got replaced in the PricingCard Alert
  • Rewrites Stripe webhook listener to support more complicated cases of unsubscribing

How should this be tested?

Run the stripe cli locally, authenticate, switch to test mode and start testing various cases!

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read How we Code at Formbricks
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand bits
  • Ran pnpm build
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Formbricks Docs if changes were necessary

Copy link

vercel bot commented Feb 15, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Ignored Deployments
Name Status Preview Comments Updated (UTC)
formbricks-cloud ⬜️ Ignored (Inspect) Visit Preview Feb 19, 2024 3:31pm
formbricks-com ⬜️ Ignored (Inspect) Visit Preview Feb 19, 2024 3:31pm

Copy link
Contributor

github-actions bot commented Feb 15, 2024

Thank you for following the naming conventions for pull request titles! 🙏

Copy link
Contributor

packages/ee/billing/handlers/subscriptionCreatedOrUpdated.ts

It's recommended to reduce the number of API calls in the handleSubscriptionUpdatedOrCreated function. Instead of retrieving the product for each item in the loop, you can retrieve all products at once and then find the needed product in the array. This will reduce the number of API calls and improve the performance of the function.
Create Issue
See the diff
Checkout the fix

    // Retrieve all products at once
    const products = await stripe.products.list();

    for (const item of stripeSubscriptionObject.items.data) {
      // Find the needed product in the array
      const product = products.data.find(p => p.id === item.price.product);

      // Rest of the code...
    }
git fetch origin && git checkout -b ReviewBot/Impro-uyxz7pz origin/ReviewBot/Impro-uyxz7pz

Comment on lines 65 to 161
updatedFeatures.inAppSurvey.status = "active";
}
} else {
// Current subscription is not scheduled to cancel at the end of the period
updatedFeatures.inAppSurvey.status = "active";
// If the current subscription is unlimited, we don't need to report usage
if (isInAppSurveyUnlimited) {
updatedFeatures.inAppSurvey.unlimited = true;
} else {
const countForTeam = await getMonthlyTeamResponseCount(team.id);
await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.inAppSurvey,
countForTeam
);
}
}
break;

case StripeProductNames.linkSurvey:
if (
!(
stripeSubscriptionObject.cancel_at_period_end &&
team.billing.features.linkSurvey.status === "cancelled"
)
) {
const isLinkSurveyUnlimited = item.price.lookup_key === StripePriceLookupKeys.linkSurveyUnlimited;

if (stripeSubscriptionObject.cancel_at_period_end) {
const isLinkSurveyScheduled = await isProductScheduled(
scheduledSubscriptions,
StripeProductNames.linkSurvey
);

if (team.billing.features.linkSurvey.status === "active" && !isLinkSurveyScheduled) {
team.billing.features.linkSurvey.status = "cancelled";
}

if (isLinkSurveyScheduled) {
updatedFeatures.linkSurvey.status = "active";
}
} else {
updatedFeatures.linkSurvey.status = "active";
}
if (item.price.lookup_key === StripePriceLookupKeys.linkSurveyUnlimited) {
updatedFeatures.linkSurvey.unlimited = true;
if (isLinkSurveyUnlimited) {
updatedFeatures.inAppSurvey.unlimited = true;
}
}
break;

case StripeProductNames.userTargeting:
if (
!(
stripeSubscriptionObject.cancel_at_period_end &&
team.billing.features.userTargeting.status === "cancelled"
)
) {
updatedFeatures.userTargeting.status = "active";
}
if (item.price.lookup_key === StripePriceLookupKeys.userTargetingUnlimited) {
updatedFeatures.userTargeting.unlimited = true;
} else {
const countForTeam = await getMonthlyActiveTeamPeopleCount(team.id);
const isUserTargetingUnlimited =
item.price.lookup_key === StripePriceLookupKeys.userTargetingUnlimited;

await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.userTargeting,
countForTeam
if (stripeSubscriptionObject.cancel_at_period_end) {
const isUserTargetingScheduled = await isProductScheduled(
scheduledSubscriptions,
StripeProductNames.userTargeting
);

if (team.billing.features.userTargeting.status === "active" && !isUserTargetingScheduled) {
team.billing.features.userTargeting.status = "cancelled";
}

if (isUserTargetingScheduled) {
updatedFeatures.userTargeting.status = "active";
}
} else {
updatedFeatures.userTargeting.status = "active";
if (isUserTargetingUnlimited) {
updatedFeatures.userTargeting.unlimited = true;
} else {
const countForTeam = await getMonthlyActiveTeamPeopleCount(team.id);
await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.userTargeting,
countForTeam
);
}
}
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Reducing the number of API calls in the handleSubscriptionUpdatedOrCreated function by retrieving all products at once and then finding the needed product in the array. This will improve the performance of the function.

Suggested change
for (const item of stripeSubscriptionObject.items.data) {
const product = await stripe.products.retrieve(item.price.product as string);
switch (product.name) {
case StripeProductNames.inAppSurvey:
if (
!(
stripeSubscriptionObject.cancel_at_period_end &&
team.billing.features.inAppSurvey.status === "cancelled"
)
) {
updatedFeatures.inAppSurvey.status = "active";
}
if (item.price.lookup_key === StripePriceLookupKeys.inAppSurveyUnlimited) {
updatedFeatures.inAppSurvey.unlimited = true;
} else {
const countForTeam = await getMonthlyTeamResponseCount(team.id);
const isInAppSurveyUnlimited = item.price.lookup_key === StripePriceLookupKeys.inAppSurveyUnlimited;
await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.inAppSurvey,
countForTeam
// If the current subscription is scheduled to cancel at the end of the period
if (stripeSubscriptionObject.cancel_at_period_end) {
// Check if the team has a scheduled subscription for this product
const isInAppProductScheduled = await isProductScheduled(
scheduledSubscriptions,
StripeProductNames.inAppSurvey
);
// If the team does not have a scheduled subscription for this product, we cancel the feature
if (team.billing.features.inAppSurvey.status === "active" && !isInAppProductScheduled) {
team.billing.features.inAppSurvey.status = "cancelled";
}
// If the team has a scheduled subscription for this product, we don't cancel the feature
if (isInAppProductScheduled) {
updatedFeatures.inAppSurvey.status = "active";
}
} else {
// Current subscription is not scheduled to cancel at the end of the period
updatedFeatures.inAppSurvey.status = "active";
// If the current subscription is unlimited, we don't need to report usage
if (isInAppSurveyUnlimited) {
updatedFeatures.inAppSurvey.unlimited = true;
} else {
const countForTeam = await getMonthlyTeamResponseCount(team.id);
await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.inAppSurvey,
countForTeam
);
}
}
break;
case StripeProductNames.linkSurvey:
if (
!(
stripeSubscriptionObject.cancel_at_period_end &&
team.billing.features.linkSurvey.status === "cancelled"
)
) {
const isLinkSurveyUnlimited = item.price.lookup_key === StripePriceLookupKeys.linkSurveyUnlimited;
if (stripeSubscriptionObject.cancel_at_period_end) {
const isLinkSurveyScheduled = await isProductScheduled(
scheduledSubscriptions,
StripeProductNames.linkSurvey
);
if (team.billing.features.linkSurvey.status === "active" && !isLinkSurveyScheduled) {
team.billing.features.linkSurvey.status = "cancelled";
}
if (isLinkSurveyScheduled) {
updatedFeatures.linkSurvey.status = "active";
}
} else {
updatedFeatures.linkSurvey.status = "active";
}
if (item.price.lookup_key === StripePriceLookupKeys.linkSurveyUnlimited) {
updatedFeatures.linkSurvey.unlimited = true;
if (isLinkSurveyUnlimited) {
updatedFeatures.inAppSurvey.unlimited = true;
}
}
break;
case StripeProductNames.userTargeting:
if (
!(
stripeSubscriptionObject.cancel_at_period_end &&
team.billing.features.userTargeting.status === "cancelled"
)
) {
updatedFeatures.userTargeting.status = "active";
}
if (item.price.lookup_key === StripePriceLookupKeys.userTargetingUnlimited) {
updatedFeatures.userTargeting.unlimited = true;
} else {
const countForTeam = await getMonthlyActiveTeamPeopleCount(team.id);
const isUserTargetingUnlimited =
item.price.lookup_key === StripePriceLookupKeys.userTargetingUnlimited;
await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.userTargeting,
countForTeam
if (stripeSubscriptionObject.cancel_at_period_end) {
const isUserTargetingScheduled = await isProductScheduled(
scheduledSubscriptions,
StripeProductNames.userTargeting
);
if (team.billing.features.userTargeting.status === "active" && !isUserTargetingScheduled) {
team.billing.features.userTargeting.status = "cancelled";
}
if (isUserTargetingScheduled) {
updatedFeatures.userTargeting.status = "active";
}
} else {
updatedFeatures.userTargeting.status = "active";
if (isUserTargetingUnlimited) {
updatedFeatures.userTargeting.unlimited = true;
} else {
const countForTeam = await getMonthlyActiveTeamPeopleCount(team.id);
await reportUsage(
stripeSubscriptionObject.items.data,
ProductFeatureKeys.userTargeting,
countForTeam
);
}
}
break;
}
const products = await stripe.products.list();
for (const item of stripeSubscriptionObject.items.data) {
const product = products.data.find(p => p.id === item.price.product);
// Rest of the code...
}

Copy link
Member

@mattinannt mattinannt left a comment

Choose a reason for hiding this comment

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

@ShubhamPalriwala thanks for the PR 😊 Just a small concern:

@@ -24,6 25,8 @@ export async function upgradePlanAction(
if (!isAuthorized) throw new AuthorizationError("Not authorized");

const subscriptionSession = await createSubscription(teamId, environmentId, priceLookupKeys);

teamCache.revalidate({ id: teamId });
Copy link
Member

Choose a reason for hiding this comment

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

why do we need to revalidate the cache here? We should only revalidate the cache after making the database call that changes the team object. Where is the call made? Can we move the revalidate command after the db call instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh apologies! it already happens inside the updateTeam service called inside the ee/billing method! Same for below, removed the both. Pushed 🤞🏼

@@ -58,5 61,6 @@ export async function removeSubscriptionAction(

const removedSubscription = await removeSubscription(teamId, environmentId, priceLookupKeys);

teamCache.revalidate({ id: teamId });
Copy link
Member

Choose a reason for hiding this comment

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

same concern as above

Copy link
Member

@mattinannt mattinannt left a comment

Choose a reason for hiding this comment

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

💪 ready to merge 😊

@mattinannt mattinannt added this pull request to the merge queue Feb 20, 2024
Merged via the queue into main with commit f69e8e8 Feb 20, 2024
13 checks passed
@mattinannt mattinannt deleted the shubham/for-1892-bug-unsubscribing-doesnt-work branch February 20, 2024 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants