Skip to content

Commit

Permalink
Add new GraphQL Goal object and resolver on Account.goal
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavlrsn committed Oct 24, 2024
1 parent 8d7163f commit 801d387
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 3 deletions.
12 changes: 12 additions & 0 deletions server/constants/goal-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Constants for the goal type
*
*/

enum GoalTypes {
ALL_TIME = 'ALL_TIME',
MONTHLY = 'MONTHLY',
YEARLY = 'YEARLY',
}

export default GoalTypes;
78 changes: 78 additions & 0 deletions server/graphql/schemaV2.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,7 @@ interface Account {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
}

"""
Expand Down Expand Up @@ -3430,6 +3431,7 @@ type Host implements Account & AccountWithContributions {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -6265,6 +6267,54 @@ type TransactionsAmountGroup {
expenseType: ExpenseType
}

type Goal {
"""
The type of the goal (per month, per year or all time)
"""
type: GoalType

"""
The amount of the goal
"""
amount: Amount

"""
The progress of the goal in percentage
"""
progress: Int
contributors(
"""
The number of results to fetch (default 10, max 1000)
"""
limit: Int! = 10

Check notice on line 6290 in server/graphql/schemaV2.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector - Schema v2

Type 'Goal' was added

Type 'Goal' was added
"""
The offset to use to fetch
"""
offset: Int! = 0
): AccountCollection
}

"""
All supported goal types
"""
enum GoalType {
"""
Total contributions
"""
ALL_TIME

"""
Contributions per month
"""
MONTHLY

"""
Contributions per year
"""
YEARLY
}

"""
A collection of webhooks
"""
Expand Down Expand Up @@ -8413,6 +8463,7 @@ type Bot implements Account {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -9260,6 +9311,7 @@ type Collective implements Account & AccountWithHost & AccountWithContributions
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -10560,6 +10612,7 @@ type Event implements Account & AccountWithHost & AccountWithContributions & Acc
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -11640,6 +11693,7 @@ type Individual implements Account {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -12693,6 +12747,7 @@ type Organization implements Account & AccountWithContributions {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -13637,6 +13692,7 @@ type Vendor implements Account & AccountWithContributions {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -17213,6 +17269,7 @@ type Fund implements Account & AccountWithHost & AccountWithContributions {
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -18173,6 +18230,7 @@ type Project implements Account & AccountWithHost & AccountWithContributions & A
EXPERIMENTAL (this may change or be removed)
"""
transactionReports(timeUnit: TimeUnit = MONTH, dateFrom: DateTime, dateTo: DateTime): TransactionReports
goal: Goal
webhooks(
"""
The number of results to fetch (default 10, max 1000)
Expand Down Expand Up @@ -19210,6 +19268,18 @@ type Mutation {
emailConfirmationToken: String!
): ConfirmGuestAccountResponse!

"""
Set a goal for your account.
"""
setGoal(
account: AccountReferenceInput!

"""
The goal to set for the account. Setting goal to undefined or null will remove any current goal.
"""
goal: GoalInput
): Goal

"""
Apply to an host with a collective. Scope: "account".
"""
Expand Down Expand Up @@ -21479,6 +21549,14 @@ type ConfirmGuestAccountResponse {
accessToken: String!
}

"""
Input type for Goals
"""
input GoalInput {
type: GoalType!
amount: Int!
}

type ProcessHostApplicationResponse {
"""
The account that applied to the host
Expand Down
19 changes: 19 additions & 0 deletions server/graphql/v2/enum/GoalType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GraphQLEnumType } from 'graphql';

import goalType from '../../../constants/goal-types';

export const GraphQLGoalType = new GraphQLEnumType({
name: 'GoalType',
description: 'All supported goal types',
values: {
[goalType.ALL_TIME]: {
description: 'Total contributions',
},
[goalType.MONTHLY]: {
description: 'Contributions per month',
},
[goalType.YEARLY]: {
description: 'Contributions per year',
},
},
});
12 changes: 12 additions & 0 deletions server/graphql/v2/input/GoalInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GraphQLInputObjectType, GraphQLInt, GraphQLNonNull } from 'graphql';

import { GraphQLGoalType } from '../enum/GoalType';

export const GraphQLGoalInput = new GraphQLInputObjectType({
name: 'GoalInput',
description: 'Input type for Goals',
fields: () => ({
type: { type: new GraphQLNonNull(GraphQLGoalType) },
amount: { type: new GraphQLNonNull(GraphQLInt) },
}),
});
32 changes: 32 additions & 0 deletions server/graphql/v2/interface/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Order, Sequelize } from 'sequelize';

import { CollectiveType } from '../../../constants/collectives';
import FEATURE from '../../../constants/feature';
import GoalTypes from '../../../constants/goal-types';
import { buildSearchConditions } from '../../../lib/sql-search';
import { getCollectiveFeed } from '../../../lib/timeline';
import { getAccountReportNodesFromQueryResult } from '../../../lib/transaction-reports';
Expand Down Expand Up @@ -66,6 +67,7 @@ import { GraphQLAccountStats } from '../object/AccountStats';
import { GraphQLActivity } from '../object/Activity';
import { GraphQLActivitySubscription } from '../object/ActivitySubscription';
import { GraphQLConnectedAccount } from '../object/ConnectedAccount';
import { GraphQLGoal } from '../object/Goal';
import { GraphQLLegalDocument } from '../object/LegalDocument';
import { GraphQLLocation } from '../object/Location';
import { GraphQLMemberInvitation } from '../object/MemberInvitation';
Expand Down Expand Up @@ -992,6 +994,36 @@ const accountFieldsDefinition = () => ({
};
},
},
goal: {
type: GraphQLGoal,
async resolve(account, _, req) {
const goal = account.settings.goal;
if (!goal) {
return null;
}

let currentAmountProgress;

if (goal.type === GoalTypes.MONTHLY) {
currentAmountProgress = (await account.getYearlyBudget({ loaders: req.loaders })) / 12;
} else if (goal.type === GoalTypes.YEARLY) {
currentAmountProgress = await account.getYearlyBudget({ loaders: req.loaders });
} else {
currentAmountProgress = await account.getTotalAmountReceived({ loaders: req.loaders, net: true });
}
const progress = Math.floor((currentAmountProgress / goal.amount) * 100);

return {
...goal,
amount: {
value: goal.amount,
currency: account.currency,
},
progress,
accountId: account.id,
};
},
},
});

export const GraphQLAccount = new GraphQLInterfaceType({
Expand Down
72 changes: 72 additions & 0 deletions server/graphql/v2/mutation/GoalMutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { GraphQLNonNull } from 'graphql';
import { cloneDeep, set } from 'lodash';

import activities from '../../../constants/activities';
import models, { sequelize } from '../../../models';
import { checkRemoteUserCanUseAccount } from '../../common/scope-check';
import { Forbidden, Unauthorized } from '../../errors';
import { fetchAccountWithReference, GraphQLAccountReferenceInput } from '../input/AccountReferenceInput';
import { GraphQLGoalInput } from '../input/GoalInput';
import { GraphQLGoal } from '../object/Goal';

const goalMutations = {
setGoal: {
type: GraphQLGoal,
description: 'Set a goal for your account.',
args: {
account: {
type: new GraphQLNonNull(GraphQLAccountReferenceInput),
},
goal: {
type: GraphQLGoalInput,
description: 'The goal to set for the account. Setting goal to undefined or null will remove any current goal.',
},
},
async resolve(_: void, args: Record<string, unknown>, req: Express.Request): Promise<typeof GraphQLGoal> {
if (!req.remoteUser) {
throw new Unauthorized();
}

return sequelize.transaction(async transaction => {
const account = await fetchAccountWithReference(args.account, {
throwIfMissing: true,
lock: true,
dbTransaction: transaction,
});

if (!req.remoteUser.isAdminOfCollective(account)) {
throw new Forbidden();
}

checkRemoteUserCanUseAccount(req);

const settings = account.settings ? cloneDeep(account.settings) : {};
set(settings, 'goal', args.goal);
// Remove legacy goals
set(settings, 'goals', undefined);
const previousData = {
settings: { goal: account.settings?.goal, ...(account.settings?.goals && { goals: account.settings.goals }) },
};
const updatedAccount = await account.update({ settings }, { transaction });
await models.Activity.create(
{
type: activities.COLLECTIVE_EDITED,
UserId: req.remoteUser.id,
UserTokenId: req.userToken?.id,
CollectiveId: account.id,
FromCollectiveId: account.id,
HostCollectiveId: account.approvedAt ? account.HostCollectiveId : null,
data: {
previousData,
newData: { settings: { goal: args.goal } },
},
},
{ transaction },
);
return updatedAccount.settings.goal;
});
},
},
};

export default goalMutations;
2 changes: 2 additions & 0 deletions server/graphql/v2/mutation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import createProjectMutation from './CreateProjectMutation';
import emojiReactionMutations from './EmojiReactionMutations';
import expenseMutations from './ExpenseMutations';
import guestMutations from './GuestMutations';
import goalMutations from './GoalMutations';
import hostApplicationMutations from './HostApplicationMutations';
import individualMutations from './IndividualMutations';
import { legalDocumentsMutations } from './LegalDocumentsMutations';
Expand Down Expand Up @@ -55,6 +56,7 @@ const mutation = {
...emojiReactionMutations,
...expenseMutations,
...guestMutations,
...goalMutations,
...hostApplicationMutations,
...individualMutations,
...legalDocumentsMutations,
Expand Down
Loading

0 comments on commit 801d387

Please sign in to comment.