Thehollidayinn/group plans part 2 (#8262)

* Added all ui components back

* Added group ui items back and initial group approval directive

* Added approval list view with approving functionality

* Added notification display for group approvals

* Fixed linting issues

* Removed expectation from beforeEach

* Moved string to locale

* Added per use group plan for stripe

* Added tests for stripe group plan upgrade

* Removed paypal option

* Abstract sub blocks. Hit group sub block from user settings page. Added group subscriptin beneifts display

* Fixed lint issue

* Added pricing and adjusted styles

* Moved text to translations

* Added group email types

* Fixed typo

* Fixed group plan abstraction and other style issues

* Fixed email unit test

* Added type to group plan to filter our group plans

* Removed dev protection from routes

* Removed hard coding and fixed upgrade plan

* Added error when group has subscription and tries to remove

* Fixed payment unit tests

* Added custom string and moved subscription check up in the logic

* Added ability for old leader to delete subscription the created

* Allowed old guild leader to edit their group subscription

* Fixed linting and tests

* Added group sub page to user sub settings

* Added approval and group tasks requests back. Hid user group sub on profile

* Added group tasks sync after adding to allow for editing

* Fixed promise chain when resolving group

* Added approvals to group promise chain

* Ensured compelted group todos are not delted at cron

* Updated copy and other minor styles

* Added group field to tags and recolored group tag.

* Added chat message when task is claimed

* Preventing task scoring when approval is needed

* Added approval requested indicator

* Updated column with for tasks on group page

* Added checklist sync on assign

* Added sync for checklist items

* Added checkilist sync when task is updated

* Added checklist sync remove

* Sanatized group tasks when updated

* Fixed lint issues

* Added instant scoring of approved task

* Added task modal

* Fixed editing of challenge and group tasks

* Added cancel button

* Added add new checklist option to update sync

* Added remove for checklist

* Added checklist update

* Added difference check and sync for checklist if there is a diff

* Fixed task syncing

* Fixed linting issues

* Fixed styles and karma tests

* Fixed minor style issues

* Fixed obj transfer on scope

* Fixed broken tests

* Added new benefits page

* Updated group page styles

* Updated benefits page style

* Added translations

* Prevented sync with empty trask list

* Added task title to edit modal

* Added new group plans page and upgrade redirect

* Added group plans redirect to upgrade

* Fixed party home page being hidden and home button click

* Fixed dynamic changing of task status and grey popup

* Fixed tag editing

* Hid benifites information if group has subscription

* Added quotes to task name

* Fixed issue with assigning multiple users

* Added new group plans ctrl

* Hid menu from public guilds

* Fixed task sync issue

* Updated placeholder for assign field

* Added correct cost to subscribe details

* Hid create, edit, delete task options from non group leaders

* Prevented some front end modifications to group tasks

* Hid tags option from group original task

* Added refresh for approvals and group tasks

* Prepend new group tasks

* Fix last checklist item sync

* Fixed casing issue with tags

* Added claimed by message on hover

* Prevent user from deleting assigned task

* Added single route for group plan sign up and payments

* Abstracted stripe payments and added initial tests

* Abstracted amazon and added initial tests

* Fixed create group message

* Update group id check and return group

* Updated to use the new returned group

* Fixed linting and promise issues

* Fixed broken leave test after merge issue

* Fixed undefined approval error and editing/deleting challenge tasks

* Add pricing to group plans, removed confirmation, and fixed redirect after payment

* Updated group plan cost text
This commit is contained in:
Keith Holliday
2016-12-21 13:45:45 -06:00
committed by GitHub
parent 55a8eef3e1
commit ea24eeb019
70 changed files with 2002 additions and 736 deletions
+191 -1
View File
@@ -4,15 +4,20 @@ import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import stripeModule from 'stripe';
import moment from 'moment';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
import amzLib from '../../../../../website/server/libs/amazonPayments';
describe('payments/index', () => {
let user, group, data, plan;
let stripe = stripeModule('test');
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
@@ -625,7 +630,40 @@ describe('payments/index', () => {
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
});
});
});
@@ -732,4 +770,156 @@ describe('payments/index', () => {
});
});
});
describe('#upgradeGroupPlan', () => {
let spy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.false;
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
});
});
describe('payWithStripe', () => {
let spy;
let stripeCreateCustomerSpy;
let createSubSpy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
subscriptions: {
data: [{id: 'test-id'}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let token = 'test-token';
let gift;
let sub = data.sub;
let groupId = group._id;
let email = 'test@test.com';
let headers = {};
let coupon;
await api.payWithStripe([
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
], stripe);
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
describe('subscribeWithAmazon', () => {
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(function () {
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let billingAgreementId = 'billingAgreementId';
let sub = data.sub;
let coupon;
let groupId = group._id;
let headers = {};
await api.subscribeWithAmazon([
billingAgreementId,
sub,
coupon,
sub,
user,
groupId,
headers,
]);
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
});
+20 -1
View File
@@ -1,13 +1,16 @@
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { BadRequest } from '../../../../../website/server/libs/errors';
import {
BadRequest,
} from '../../../../../website/server/libs/errors';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
import validator from 'validator';
import { TAVERN_ID } from '../../../../../website/common/script/';
import { v4 as generateUUID } from 'uuid';
import shared from '../../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
@@ -638,6 +641,22 @@ describe('Group Model', () => {
expect(party).to.not.exist;
});
it('does not delete a private group when the last member leaves and a subscription is active', async () => {
party.memberCount = 1;
party.purchased.plan.customerId = '110002222333';
await expect(party.leave(participatingMember))
.to.eventually.be.rejected.and.to.eql({
name: 'NotAuthorized',
httpCode: 401,
message: shared.i18n.t('cannotDeleteActiveGroup'),
});
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
expect(party.memberCount).to.eql(1);
});
it('does not delete a public group when the last member leaves', async () => {
party.privacy = 'public';
+129 -25
View File
@@ -2,7 +2,7 @@ import { model as Challenge } from '../../../../../website/server/models/challen
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { each, find } from 'lodash';
import { each, find, findIndex } from 'lodash';
describe('Group Task Methods', () => {
let guild, leader, challenge, task;
@@ -68,11 +68,29 @@ describe('Group Task Methods', () => {
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
task.group.id = guild._id;
await task.save();
if (task.checklist) {
task.checklist.push({
text: 'Checklist Item 1',
completed: false,
});
}
});
it('syncs an assigned task to a user', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({_id: leader._id});
let tagIndex = findIndex(updatedLeader.tags, {id: guild._id});
let newTag = updatedLeader.tags[tagIndex];
expect(newTag.id).to.equal(guild._id);
expect(newTag.name).to.equal(guild.name);
expect(newTag.group).to.equal(guild._id);
});
it('create tags for a user when task is synced', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
@@ -96,38 +114,124 @@ describe('Group Task Methods', () => {
expect(syncedTask.text).to.equal(task.text);
});
it('syncs updated info for assigned task to all users', async () => {
let newMember = new User({
guilds: [guild._id],
});
await newMember.save();
it('syncs checklist items to an assigned user', async () => {
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
if (task.type !== 'daily' && task.type !== 'todo') return;
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
});
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
describe('syncs updated info', async() => {
let newMember;
beforeEach(async () => {
newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
});
it('syncs updated info for assigned task to all users', async () => {
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('syncs a new checklist item to all assigned users', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
let newCheckListItem = {
text: 'Checklist Item 1',
completed: false,
};
task.checklist.push(newCheckListItem);
await guild.updateTask(task, {newCheckListItem});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
});
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
let updateCheckListText = 'Updated checklist item';
if (task.checklist) {
task.checklist[0].text = updateCheckListText;
}
await guild.updateTask(task, {updateCheckListItems: [task.checklist[0]]});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
});
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
await guild.updateTask(task, {removedCheckListItemId: task.checklist[0].id});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(0);
expect(syncedMemberTask.checklist.length).to.equal(0);
});
});
it('removes an assigned task and unlinks assignees', async () => {