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

AppPlans.set() doesn't remove old plan. #8

Open
MichaelJCole opened this issue Jan 30, 2017 · 9 comments
Open

AppPlans.set() doesn't remove old plan. #8

MichaelJCole opened this issue Jan 30, 2017 · 9 comments

Comments

@MichaelJCole
Copy link

MichaelJCole commented Jan 30, 2017

Hi, I have three plans: freemium, theDeal, tooExpensive

The intention is that they are exclusive. A user can have only one - default is freemium

If a user does freemium -> theDeal -> tooExpensive -> freemium, they end up with two active subscriptions, both cancelling at the end of the month.

I'm not sure where this fits into the API for this module. Is it still under active development?

@aldeed
Copy link
Collaborator

aldeed commented Feb 8, 2017

@MichaelJCole We can look into it more easily if you can post as much as possible of the plan definitions and what the plan settings are in Stripe, and the reproduction steps (I'm assuming just call .set 4 times).

The code appears to remove all existing plans first, so a detailed reproduction is necessary to figure out where there might be a bug for your use case.

@MichaelJCole
Copy link
Author

MichaelJCole commented Feb 9, 2017

Hi @aldeed, I ended up rolling my own. There was some logic in upgrading/downgrading that was specific to my app (upgrades are pro-rated. downgrades finish the month).

I implemented "finish the month" as cancelling at the end of the period, then setting up a "trialing" subscription for the next month plan.

This blog post about await and async made npm Stripe much easier to work with.

Here's the basics if you're interested. (I kept using AppPlans as the name because it's a good name). AppPlans.sync() fetches the stripe customer to a private property (user.privateStripeCustomer), then summarizes the plans in a public property to be sent to the client (user.appPlans.current and .next).

Anyways, thanks for this package, it was very helpful to design a direction :-)

// lib/AppPlans.js
if (typeof AppPlans === 'undefined') AppPlans = {};
AppPlans = _.extend(AppPlans, {
  defaultPlan: 'betaFreemium',
  plans: [
    {
      id: 'betaFreemium',
      name: 'Freemium',
      amount: 0,
      amountCurrency: 'usd',
      description: 'Freemium Plan',
      enabled: true,
    },
    {
      id: 'betaStarter',
      name: 'Starter',
      amount: 1500,
      amountCurrency: 'usd',
      description: 'Starter Plan',
      enabled: true,
    },

Then these Methods on the server called directly from client code:

// Meteor Methods

Meteor.methods({
  AppPlan_sync() {
    if (!Meteor.user()) throw new Meteor.Error('accessDenied', 'access denied');
    return AppPlans.sync(Meteor.userId());
  },
  async AppPlan_setPlan(newPlanId, token) {
    check(newPlanId, String);
    if (token) check(token, Schema.StripeToken);
    var u = Meteor.user();
    if (!u) throw new Meteor.Error('accessDenied', 'access denied');
    return AppPlans.setPlan(u._id, newPlanId, token);  // configures new plans then calls AppPlans.sync().
  },
  async AppPlan_updateCard(token) {
    check(token, Schema.StripeToken);
    var u = Meteor.user();
    if (!u) throw new Meteor.Error('accessDenied', 'access denied');

    // sync user
    try {
      var newSource = await Stripe.customers.update(Meteor.user().stripeCustomer.id, {
        source: token.id,
      });
      if (!newSource) throw new Meteor.Error('payment', 'Could not create source from token');
      return AppPlans.sync(Meteor.userId());
    } catch (e) {
      Log.error(e.message || e);
      throw new Meteor.Error('error', e.message || e);
    }
  }
});

Accounts.onLogin(function (info) {
  AppPlans.sync(info.user._id); // Autoset's default plan if needed
});

@aldeed
Copy link
Collaborator

aldeed commented Feb 9, 2017

@MichaelJCole Thanks for the details. This pkg does finish the month when you cancel also, but I think it needs work in the area of switching from one paid plan to another like you said.

I will reopen so we can take a look eventually.

@aldeed aldeed reopened this Feb 9, 2017
@advancedsoftwarecanada
Copy link

Hi @aldeed I'm here with my app that has the option to upgrade plans. Maybe I just need to change the billing process, but the upsell on my app is from $10 to $50.

The issue is that the subscription is not being ended.

image

image

image

@advancedsoftwarecanada
Copy link

// BROKEN?
// AppPlans.remove('sco_elite', {userId: userId});
// AppPlans.remove('sco_elder', {userId: userId});
// AppPlans.remove('sco_elder', function (error) {console.log(error)});
// AppPlans.remove('sco_elite', {userId: Meteor.userId()});
// AppPlans.remove('sco_elder', {userId: Meteor.userId()});

@advancedsoftwarecanada
Copy link

advancedsoftwarecanada commented May 26, 2017

`
AppPlans.define('sco_gm', {
//includedPlans: ['sco_free']
});

AppPlans.define('sco_free', {
  //includedPlans: ['sco_free']
});

AppPlans.define('sco_elite', {
  services: [
	{
	  name: 'stripe', // External plan is on Stripe
	  planName: 'sco_elite', // External plan ID is "silver"
	  // Options for the Stripe Checkout flow on the client
	  payOptions: {
		name: 'SCO Elite',
		description: '$10.00/month',
		amount: 1000
	  }
	}
  ],
  //includedPlans: ['sco_free']
});

AppPlans.define('sco_elder', {
  services: [
	{
	  name: 'stripe', // External plan is on Stripe
	  planName: 'sco_elder', // External plan ID is "silver"
	  // Options for the Stripe Checkout flow on the client
	  payOptions: {
		name: 'SCO Edler',
		description: '$50.00/month',
		amount: 5000
	  }
	}
  ],
 // includedPlans: ['sco_free']
});

`

@advancedsoftwarecanada
Copy link

Okay so been digging in this code all night. My eyes hurt. Great plugin, I must say.

Unsubscribing doesn't IMMEDIATELY end your subscription. It waits until the subscription duration has passed. It makes sense, we took their money, let them have service until such a time.

It appears though, upon doing a SYNC, this is when the use will truely drop out of being enrolled in that plan on your Meteor install.

So, the code is correct. That's the way you want it.

What we need is a SWAP, which has been identified in another issue.

@advancedsoftwarecanada
Copy link

So it appear to be almost 2.5 years later and I am back to this issue lol.

I have gotten smarter, and realize your package needs some additional methods that will help solve this: membership_highest_role("paid") for example

`
// Membership Roles
Template.registerHelper('membership_highest_role',(roleCheck) => {

var highest_paid_role = "sco_free";
if(AppPlans.hasAccess('sco_lieutenant')){
	highest_paid_role = "sco_lieutenant";
}
if(AppPlans.hasAccess('sco_elite')){
	highest_paid_role = "sco_elite";
}
if(AppPlans.hasAccess('sco_elder')){
	highest_paid_role = "sco_elder";
}

if(highest_paid_role == "sco_free" && roleCheck == "sco_free"){
	return true;
}
if(highest_paid_role == "sco_lieutenant" && roleCheck == "sco_lieutenant"){
	return true;
}
if(highest_paid_role == "sco_elite" && roleCheck == "sco_elite"){
	return true;
}
if(highest_paid_role == "sco_elder" && roleCheck == "sco_elder"){
	return true;
}

return false;

});

Template.registerHelper('membership_has_cancellation_date',() => {

var highest_paid_role = "sco_free";
if(AppPlans.hasAccess('sco_lieutenant')){
	highest_paid_role = "sco_lieutenant";
	if(AppPlans.endDate("sco_lieutenant")){
		return true;
	}
}
if(AppPlans.hasAccess('sco_elite')){
	highest_paid_role = "sco_elite";
	if(AppPlans.endDate("sco_elite")){
		return true;
	}
}
if(AppPlans.hasAccess('sco_elder')){
	highest_paid_role = "sco_elder";
	if(AppPlans.endDate("sco_elder")){
		return true;
	}
}

return false;

});

Template.registerHelper('membership_highest_role_cancellation_date',() => {

var highest_paid_role = "sco_free";
if(AppPlans.hasAccess('sco_lieutenant')){
	highest_paid_role = "sco_lieutenant";
	return AppPlans.endDate("sco_lieutenant");
}
if(AppPlans.hasAccess('sco_elite')){
	highest_paid_role = "sco_elite";
	return AppPlans.endDate("sco_elite");
}
if(AppPlans.hasAccess('sco_elder')){
	highest_paid_role = "sco_elder";
	return AppPlans.endDate("sco_elder");
}


return false;

});
`

@aldeed
Copy link
Collaborator

aldeed commented Mar 24, 2020

I haven't been using this lately and don't remember much about it, but if you or anyone wants to submit a pull request, I can review and merge it. Or if somebody out there wants to maintain this package, let me know.

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

No branches or pull requests

3 participants