Skip to content

Commit

Permalink
[server] Introduce TeamSubscription.excludeFromMoreResources
Browse files Browse the repository at this point in the history
  • Loading branch information
geropl committed Mar 7, 2022
1 parent f141323 commit 49e29ab
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 5 deletions.
1 change: 1 addition & 0 deletions components/gitpod-db/src/team-subscription-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const TeamSubscriptionDB = Symbol('TeamSubscriptionDB');
export interface TeamSubscriptionDB {
storeTeamSubscriptionEntry(ts: TeamSubscription): Promise<void>;
findTeamSubscriptionById(id: string): Promise<TeamSubscription | undefined>;
findTeamSubscriptionBySlotId(slotId: string): Promise<TeamSubscription | undefined>;
findTeamSubscriptionByPaymentRef(userId: string, paymentReference: string): Promise<TeamSubscription | undefined>;
findTeamSubscriptionsForUser(userId: string, date: string): Promise<TeamSubscription[]>;
findTeamSubscriptions(partial: DeepPartial<TeamSubscription>): Promise<TeamSubscription[]>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ export class DBTeamSubscription implements TeamSubscription {
default: false
})
deleted?: boolean;

@Column({
default: false
})
excludeFromMoreResources?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import {MigrationInterface, QueryRunner} from "typeorm";
import { columnExists } from "./helper/helper";

const TABLE_NAME = "d_b_team_subscription";
const COLUMN_NAME = "excludeFromMoreResources";

export class TSExcludeFromMoreResources1646410374581 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await columnExists(queryRunner, TABLE_NAME, COLUMN_NAME))) {
await queryRunner.query(`ALTER TABLE ${TABLE_NAME} ADD COLUMN ${COLUMN_NAME} tinyint(4) NOT NULL DEFAULT '0', ALGORITHM=INPLACE, LOCK=NONE `);
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
}

}
8 changes: 8 additions & 0 deletions components/gitpod-db/src/typeorm/team-subscription-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export class TeamSubscriptionDBImpl implements TeamSubscriptionDB {
return repo.findOne(id);
}

async findTeamSubscriptionBySlotId(slotId: string): Promise<TeamSubscription | undefined> {
const repo = await this.getRepo();
const query = repo.createQueryBuilder("ts")
.leftJoinAndMapOne("ts.id", DBTeamSubscriptionSlot, "slot", "slot.teamSubscriptionId = ts.id")
.where("slot.id = :slotId", { slotId });
return query.getOne();
}

async findTeamSubscriptionByPaymentRef(userId: string, paymentReference: string): Promise<TeamSubscription | undefined> {
const repo = await this.getRepo();
return repo.findOne({ userId, paymentReference });
Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/team-subscription-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface TeamSubscription {
paymentReference: string;
cancellationDate?: string;
deleted?: boolean;
/** If this flag is set slots are not eligibile for clusters with "more-resources" - even if their plan might be */
excludeFromMoreResources?: boolean;
}

export namespace TeamSubscription {
Expand Down
File renamed without changes.
145 changes: 145 additions & 0 deletions components/server/ee/src/user/eligibility-service.spec.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the Gitpod Enterprise Source Code License,
* See License.enterprise.txt in the project root folder.
*/

import { testContainer } from '@gitpod/gitpod-db/lib/test-container';
import { DBUser, DBIdentity, UserDB, AccountingDB, TeamSubscriptionDB } from '@gitpod/gitpod-db/lib';
import { TypeORM } from '@gitpod/gitpod-db/lib/typeorm/typeorm';
import { Subscription } from '@gitpod/gitpod-protocol/lib/accounting-protocol';
import { Plans } from '@gitpod/gitpod-protocol/lib/plans';
import * as chai from 'chai';
import { suite, test, timeout } from 'mocha-typescript';
import { Config } from '../../../src/config';
import { EligibilityService } from './eligibility-service';
import { DBSubscription } from '@gitpod/gitpod-db/lib/typeorm/entity/db-subscription';
import { DBTeamSubscription } from '@gitpod/gitpod-db/lib/typeorm/entity/db-team-subscription';
import { DBTeamSubscriptionSlot } from '@gitpod/gitpod-db/lib/typeorm/entity/db-team-subscription-slot';
import { Token, User } from '@gitpod/gitpod-protocol';
import { AccountService, AccountServiceImpl, SubscriptionService } from '@gitpod/gitpod-payment-endpoint/lib/accounting';
import { EMailDomainService, EMailDomainServiceImpl } from '../auth/email-domain-service';
import { TokenProvider } from "../../../src/user/token-provider";
import { AccountStatementProvider } from './account-statement-provider';

const expect = chai.expect;

const localTestContainer = testContainer.createChild();
localTestContainer.bind(EligibilityService).toSelf().inSingletonScope();
localTestContainer.bind(Config).toDynamicValue(ctx => ({
enablePayment: true,
} as Config)).inSingletonScope();
localTestContainer.bind(SubscriptionService).toSelf().inSingletonScope();

localTestContainer.bind(EMailDomainService).to(EMailDomainServiceImpl).inSingletonScope();

localTestContainer.bind(TokenProvider).toDynamicValue(ctx => <TokenProvider>{
getFreshPortAuthenticationToken: (user: User, host: string) => { return {} as Promise<Token> },
getTokenForHost: (user: User, workspaceId: string) => { return {} as Promise<Token> },
});

localTestContainer.bind(AccountStatementProvider).toSelf().inRequestScope();
localTestContainer.bind(AccountServiceImpl).toSelf().inSingletonScope();
localTestContainer.bind(AccountService).toService(AccountServiceImpl);

const start = new Date(Date.UTC(2000, 0, 1)).toISOString();
const userId = 'Niemand';
const tsId = 'someTeamSubscription';
const subscriptionId = 'theSubscriptionForTsSlot';
const slotId = 'someSlotId';

@timeout(10000)
@suite class AccountServiceSpec {
typeORM = localTestContainer.get<TypeORM>(TypeORM);
cut = localTestContainer.get<EligibilityService>(EligibilityService);

userDb = localTestContainer.get<UserDB>(UserDB);
accountingDb = localTestContainer.get<AccountingDB>(AccountingDB);
tsDb = localTestContainer.get<TeamSubscriptionDB>(TeamSubscriptionDB);

subscription: Subscription;
user: User;

@timeout(10000)
async before() {
await this.purgeDB();

this.user = await this.userDb.storeUser({
id: userId,
creationDate: start,
fullName: 'Herr Niemand',
identities: [{
authProviderId: 'github.com',
authId: 'Niemand',
authName: 'Niemand',
tokens: []
}],
});

this.subscription = await this.accountingDb.newSubscription({
userId,
startDate: start,
amount: 100,
planId: 'test'
});
}

protected async purgeDB() {
const manager = (await this.typeORM.getConnection()).manager;
await manager.query('SET FOREIGN_KEY_CHECKS = 0;');
await manager.clear(DBIdentity);
await manager.clear(DBUser);
await manager.query('SET FOREIGN_KEY_CHECKS = 1;');

await manager.clear(DBSubscription);
await manager.clear(DBTeamSubscription);
await manager.clear(DBTeamSubscriptionSlot);
}

protected async createTsSubscription(excludeFromMoreResources: boolean | undefined = undefined) {
const plan = Plans.TEAM_PROFESSIONAL_EUR;
const ts = await this.tsDb.storeTeamSubscriptionEntry({
id: tsId,
userId,
paymentReference: "abcdef",
planId: plan.chargebeeId,
quantity: 1,
startDate: start,
excludeFromMoreResources,
});

const slot = await this.tsDb.storeSlot({
id: slotId,
teamSubscriptionId: tsId,
assigneeId: userId,
subscriptionId,
});

const sub = await this.accountingDb.storeSubscription(Subscription.create({
userId,
planId: plan.chargebeeId,
startDate: start,
amount: Plans.getHoursPerMonth(plan),
teamSubscriptionSlotId: slot.id,
}));
return { plan, sub, ts, slot };
}

@timeout(5000)
@test async testUserGetsMoreResources() {
await this.createTsSubscription();

const actual = await this.cut.userGetsMoreResources(this.user);
expect(actual, "user with Team Unleashed gets 'more resources'").to.equal(true);
}

@timeout(5000)
@test async testUserGetsMoreResources_excludeFromMoreResources() {
await this.createTsSubscription(true);

const actual = await this.cut.userGetsMoreResources(this.user);
expect(actual, "user with Team Unleashed but excludeFromMoreResources set does not get 'more resources'").to.equal(false);
}
}

module.exports = new AccountServiceSpec()
26 changes: 22 additions & 4 deletions components/server/ee/src/user/eligibility-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
*/

import { inject, injectable } from "inversify";
import { UserDB } from "@gitpod/gitpod-db/lib";
import { HostContextProvider } from "../../../src/auth/host-context-provider";
import { TeamSubscriptionDB, UserDB } from "@gitpod/gitpod-db/lib";
import { TokenProvider } from "../../../src/user/token-provider";
import { User, WorkspaceTimeoutDuration, WorkspaceInstance } from "@gitpod/gitpod-protocol";
import { RemainingHours } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
Expand Down Expand Up @@ -47,10 +46,10 @@ export class EligibilityService {
@inject(Config) protected readonly config: Config;
@inject(UserDB) protected readonly userDb: UserDB;
@inject(SubscriptionService) protected readonly subscriptionService: SubscriptionService;
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
@inject(EMailDomainService) protected readonly domainService: EMailDomainService;
@inject(TokenProvider) protected readonly tokenProvider: TokenProvider;
@inject(AccountStatementProvider) protected readonly accountStatementProvider: AccountStatementProvider;
@inject(TeamSubscriptionDB) protected readonly teamSubscriptionDb: TeamSubscriptionDB;

/**
* Whether the given user is recognized as a student within Gitpod
Expand Down Expand Up @@ -270,7 +269,26 @@ export class EligibilityService {
Plans.TEAM_PROFESSIONAL_USD,
].map(p => p.chargebeeId);

return subscriptions.filter(s => eligblePlans.includes(s.planId!)).length > 0;
const relevantSubscriptions = subscriptions.filter(s => eligblePlans.includes(s.planId!));
if (relevantSubscriptions.length === 0) {
// user has no subscription that grants "more resources"
return false;
}

// some TeamSubscriptions are marked with 'excludeFromMoreResources' to convey that those are _not_ receiving more resources
const excludeFromMoreResources = await Promise.all(relevantSubscriptions.map(async (s): Promise<boolean> => {
if (!s.teamSubscriptionSlotId) {
return false;
}
const ts = await this.teamSubscriptionDb.findTeamSubscriptionBySlotId(s.teamSubscriptionSlotId);
return !!ts?.excludeFromMoreResources;
}));
if (excludeFromMoreResources.every(b => b)) {
// if all TS the user is part of are marked this way, we deny that privilege
return false;
}

return true;
}

protected async getUser(user: User | string): Promise<User> {
Expand Down
6 changes: 5 additions & 1 deletion components/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
"declaration": true,
"declarationMap": true,
"skipLibCheck": true,
"useUnknownInCatchVariables": false
"useUnknownInCatchVariables": false,
"typeRoots": [
"../../node_modules/@types",
"./ee/src/typings"
]
},
"include": [
"src",
Expand Down

0 comments on commit 49e29ab

Please sign in to comment.