mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Group plans subs to all (#8394)
* Added subscriptions to all members when group subs * Added unsub when group cancels * Give user a subscription when they join a subbed group * Removed subscription when user leaves or is removed from group * Fixed linting issues: * Added tests for users with a subscription being upgraded to group plan * Added tests for checking if existing recurring user sub gets updated during group plan. Added better merging for plans * Added test for existing gift subscriptions * Added additional months to user when they have an existing recurring subscription and get upgraded to group sub * Adds test for user who has cancelled with date termined in the future * Added test to ensure date termined is reset * Added tests for extra months carrying over * Added test for gems bought field * Add tests to for fields that should remain when upgrading * Added test for all payment methods * Added prevention for when a user joins a second group plan * Fixed subscribing tests * Separated group plan payment tests * Added prevention of editing a user with a unlimited sub * Add tests to ensure group keeps plan if they are in two and leave one * Ensured users with two group plans do not get cancelled when on group plan is cancelled * Ensured users without group sub are untouched when group cancels * Fixed lint issues * Added new emails * Added fix for cron tests * Add restore to stubbed methods * Ensured cancelled group subscriptions are updated * Changed group plan exist check to check for date terminated * Updated you cannont delete active group message * Removed description requirement * Added upgrade group plan for Amazon payments * Fixed lint issues * Fixed broken tests * Fixed user delete tests * Fixed function calls * Hid cancel button if user has group plan * Hide difficulty from rewards * Prevented add user functions to be called when group plan is cancelled * Fixed merge issue * Correctly displayed group price * Added message when you are about to join canclled group plan * Fixed linting issues * Updated tests to have no redirect to homes * Allowed leaving a group with a canceld subscription * Fixed spelling issues * Prevented user from changing leader with active sub * Added payment details title to replace subscription title * Ensured we do not count leader when displaying upcoming cost * Prevented party tasks from being displayed twice * Prevented cancelling and already cancelled sub * Fixed styles of subscriptions * Added more specific mystery item tests * Fixed test to refer to leader * Extended test range to account for short months * Fixed merge conflicts * Updated yarn file * Added missing locales * Trigger notification * Removed yarn * Fixed locales * Fixed scope mispelling * Fixed line endings * Removed extra advanced options from rewards * Prevent group leader from leaving an active group plan * Fixed issue with extra months applied to cancelled group plan * Ensured member count is calculated when updatedGroupPlan * Updated amazon payment method constant name * Added comment to cancel sub user method * Fixed smantic issues * Added unite test for user isSubscribed and hasNotCancelled * Add tests for isSubscribed and hasNotCanceled * Changed default days remaining to 2 days for group plans * Fixed logic with adding canceled notice to group invite
This commit is contained in:
@@ -11,19 +11,21 @@ import {
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../models/group';
|
||||
import { model as User } from '../models/user';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from './errors';
|
||||
import slack from './slack';
|
||||
import nconf from 'nconf';
|
||||
|
||||
import stripeModule from 'stripe';
|
||||
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
|
||||
|
||||
let api = {};
|
||||
|
||||
api.constants = {
|
||||
UNLIMITED_CUSTOMER_ID: 'habitrpg', // Users with the customerId have an unlimted free subscription
|
||||
GROUP_PLAN_CUSTOMER_ID: 'group-plan',
|
||||
GROUP_PLAN_PAYMENT_METHOD: 'Group Plan',
|
||||
};
|
||||
|
||||
function revealMysteryItems (user) {
|
||||
_.each(shared.content.gear.flat, function findMysteryItems (item) {
|
||||
if (
|
||||
@@ -44,6 +46,158 @@ function _dateDiff (earlyDate, lateDate) {
|
||||
return moment(lateDate).diff(earlyDate, 'months', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subscription to members of a group
|
||||
*
|
||||
* @param group The Group Model that is subscribed to a group plan
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
api.addSubscriptionToGroupUsers = async function addSubscriptionToGroupUsers (group) {
|
||||
let members;
|
||||
if (group.type === 'guild') {
|
||||
members = await User.find({guilds: group._id}).select('_id purchased').exec();
|
||||
} else {
|
||||
members = await User.find({'party._id': group._id}).select('_id purchased').exec();
|
||||
}
|
||||
|
||||
let promises = members.map((member) => {
|
||||
return this.addSubToGroupUser(member, group);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a subscription to a new member of a group
|
||||
*
|
||||
* @param member The new member of the group
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
api.addSubToGroupUser = async function addSubToGroupUser (member, group) {
|
||||
let customerIdsToIgnore = [this.constants.GROUP_PLAN_CUSTOMER_ID, this.constants.UNLIMITED_CUSTOMER_ID];
|
||||
|
||||
let data = {
|
||||
user: {},
|
||||
sub: {
|
||||
key: 'group_plan_auto',
|
||||
},
|
||||
customerId: 'group-plan',
|
||||
paymentMethod: 'Group Plan',
|
||||
headers: {},
|
||||
};
|
||||
|
||||
let plan = {
|
||||
planId: 'group_plan_auto',
|
||||
customerId: 'group-plan',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'groupPlan',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: null,
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
|
||||
if (member.isSubscribed()) {
|
||||
let memberPlan = member.purchased.plan;
|
||||
let customerHasCancelledGroupPlan = memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID && !member.hasNotCancelled();
|
||||
if (customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1 && !customerHasCancelledGroupPlan) return;
|
||||
|
||||
if (member.hasNotCancelled()) await member.cancelSubscription();
|
||||
|
||||
let today = new Date();
|
||||
plan = _.clone(member.purchased.plan.toObject());
|
||||
let extraMonths = Number(plan.extraMonths);
|
||||
if (plan.dateTerminated) extraMonths += _dateDiff(today, plan.dateTerminated);
|
||||
|
||||
_(plan).merge({ // override with these values
|
||||
planId: 'group_plan_auto',
|
||||
customerId: 'group-plan',
|
||||
dateUpdated: today,
|
||||
paymentMethod: 'groupPlan',
|
||||
extraMonths,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: null,
|
||||
owner: member._id,
|
||||
}).defaults({ // allow non-override if a plan was previously used
|
||||
gemsBought: 0,
|
||||
dateCreated: today,
|
||||
mysteryItems: [],
|
||||
}).value();
|
||||
}
|
||||
|
||||
member.purchased.plan = plan;
|
||||
|
||||
data.user = member;
|
||||
await this.createSubscription(data);
|
||||
|
||||
let leader = await User.findById(group.leader).exec();
|
||||
txnEmail(data.user, 'group-member-joining', [
|
||||
{name: 'LEADER', content: leader.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Cancels subscriptions of members of a group
|
||||
*
|
||||
* @param group The Group Model that is cancelling a group plan
|
||||
*
|
||||
* @return undefined
|
||||
*/
|
||||
api.cancelGroupUsersSubscription = async function cancelGroupUsersSubscription (group) {
|
||||
let members;
|
||||
if (group.type === 'guild') {
|
||||
members = await User.find({guilds: group._id}).select('_id guilds purchased').exec();
|
||||
} else {
|
||||
members = await User.find({'party._id': group._id}).select('_id guilds purchased').exec();
|
||||
}
|
||||
|
||||
let promises = members.map((member) => {
|
||||
return this.cancelGroupSubscriptionForUser(member, group);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
api.cancelGroupSubscriptionForUser = async function cancelGroupSubscriptionForUser (user, group) {
|
||||
if (user.purchased.plan.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) return;
|
||||
|
||||
let userGroups = _.clone(user.guilds.toObject());
|
||||
userGroups.push('party');
|
||||
|
||||
let index = userGroups.indexOf(group._id);
|
||||
userGroups.splice(index, 1);
|
||||
|
||||
let groupPlansQuery = {
|
||||
type: {$in: ['guild', 'party']},
|
||||
// privacy: 'private',
|
||||
_id: {$in: userGroups},
|
||||
'purchased.plan.dateTerminated': null,
|
||||
};
|
||||
|
||||
let groupFields = `${basicGroupFields} purchased`;
|
||||
let userGroupPlans = await Group.find(groupPlansQuery).select(groupFields).exec();
|
||||
|
||||
if (userGroupPlans.length === 0) {
|
||||
let leader = await User.findById(group.leader).exec();
|
||||
txnEmail(user, 'group-member-cancel', [
|
||||
{name: 'LEADER', content: leader.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
await this.cancelSubscription({user});
|
||||
}
|
||||
};
|
||||
|
||||
api.createSubscription = async function createSubscription (data) {
|
||||
let recipient = data.gift ? data.gift.member : data.user;
|
||||
let block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
|
||||
@@ -75,6 +229,8 @@ api.createSubscription = async function createSubscription (data) {
|
||||
emailType = 'group-subscription-begins';
|
||||
groupId = group._id;
|
||||
recipient.purchased.plan.quantity = data.sub.quantity;
|
||||
|
||||
await this.addSubscriptionToGroupUsers(group);
|
||||
}
|
||||
|
||||
plan = recipient.purchased.plan;
|
||||
@@ -135,7 +291,8 @@ api.createSubscription = async function createSubscription (data) {
|
||||
revealMysteryItems(recipient);
|
||||
}
|
||||
|
||||
if (!data.gift) {
|
||||
// @TODO: Create a factory pattern for use cases
|
||||
if (!data.gift && data.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) {
|
||||
txnEmail(data.user, emailType);
|
||||
}
|
||||
|
||||
@@ -225,22 +382,6 @@ api.createSubscription = async function createSubscription (data) {
|
||||
});
|
||||
};
|
||||
|
||||
api.updateStripeGroupPlan = async function updateStripeGroupPlan (group, stripeInc) {
|
||||
if (group.purchased.plan.paymentMethod !== 'Stripe') return;
|
||||
let stripeApi = stripeInc || stripe;
|
||||
let plan = shared.content.subscriptionBlocks.group_monthly;
|
||||
|
||||
await stripeApi.subscriptions.update(
|
||||
group.purchased.plan.subscriptionId,
|
||||
{
|
||||
plan: plan.key,
|
||||
quantity: group.memberCount + plan.quantity - 1,
|
||||
}
|
||||
);
|
||||
|
||||
group.purchased.plan.quantity = group.memberCount + plan.quantity - 1;
|
||||
};
|
||||
|
||||
// Sets their subscription to be cancelled later
|
||||
api.cancelSubscription = async function cancelSubscription (data) {
|
||||
let plan;
|
||||
@@ -248,6 +389,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
let cancelType = 'unsubscribe';
|
||||
let groupId;
|
||||
let emailType = 'cancel-subscription';
|
||||
let emailMergeData = [];
|
||||
|
||||
// If we are buying a group subscription
|
||||
if (data.groupId) {
|
||||
@@ -265,12 +407,23 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
}
|
||||
plan = group.purchased.plan;
|
||||
emailType = 'group-cancel-subscription';
|
||||
emailMergeData.push({name: 'GROUP_NAME', content: group.name});
|
||||
|
||||
await this.cancelGroupUsersSubscription(group);
|
||||
} else {
|
||||
plan = data.user.purchased.plan;
|
||||
}
|
||||
|
||||
let customerId = plan.customerId;
|
||||
let now = moment();
|
||||
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
|
||||
let defaultRemainingDays = 30;
|
||||
|
||||
if (plan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID) {
|
||||
defaultRemainingDays = 2;
|
||||
}
|
||||
|
||||
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : defaultRemainingDays;
|
||||
if (plan.extraMonths < 0) plan.extraMonths = 0;
|
||||
let extraDays = Math.ceil(30.5 * plan.extraMonths);
|
||||
let nowStr = `${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`;
|
||||
let nowStrFormat = 'MM/DD/YYYY';
|
||||
@@ -289,7 +442,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
txnEmail(data.user, emailType);
|
||||
if (customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) txnEmail(data.user, emailType, emailMergeData);
|
||||
|
||||
if (group) {
|
||||
cancelType = 'group-unsubscribe';
|
||||
|
||||
Reference in New Issue
Block a user