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

feat(api): apply member limit per billing plan #6630

Merged
merged 9 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .source
30 changes: 17 additions & 13 deletions apps/api/src/app/testing/billing/upsert-subscription.e2e-ee.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
/* eslint-disable global-require */
import sinon from 'sinon';
import { CommunityOrganizationRepository, IntegrationRepository } from '@novu/dal';
import { expect } from 'chai';
import { ApiServiceLevelEnum } from '@novu/shared';
// eslint-disable-next-line no-restricted-imports
import { StripeBillingIntervalEnum, StripeUsageTypeEnum } from '@novu/ee-billing/src/stripe/types';
import {
StripeBillingIntervalEnum,
StripeUsageTypeEnum,
StripeSubscriptionStatusEnum,
} from '@novu/ee-billing/src/stripe/types';

describe('UpsertSubscription', () => {
const eeBilling = require('@novu/ee-billing');
if (!eeBilling) {
throw new Error('ee-billing does not exist');
}

const { UpsertSubscription, GetPrices, UpsertSubscriptionCommand } = eeBilling;
const { UpsertSubscription, GetPrices, UpdateServiceLevel, UpsertSubscriptionCommand } = eeBilling;

const stripeStub = {
subscriptions: {
Expand All @@ -26,8 +29,7 @@ describe('UpsertSubscription', () => {
let deleteSubscriptionStub: sinon.SinonStub;

let getPricesStub: sinon.SinonStub;
const repo = new CommunityOrganizationRepository();
let updateOrgStub: sinon.SinonStub;
let updateServiceLevelStub: sinon.SinonStub;

const mockCustomerBase = {
id: 'customer_id',
Expand All @@ -39,6 +41,7 @@ describe('UpsertSubscription', () => {
data: [
{
id: 'subscription_id',
status: StripeSubscriptionStatusEnum.ACTIVE,
billing_cycle_anchor: 123456789,
items: {
data: [
Expand Down Expand Up @@ -69,23 +72,26 @@ describe('UpsertSubscription', () => {
},
],
} as any);
updateOrgStub = sinon.stub(repo, 'update').resolves({ matched: 1, modified: 1 });
(repo as any).integrationRepository = sinon.createStubInstance(IntegrationRepository);
updateServiceLevelStub = sinon.stub(UpdateServiceLevel.prototype, 'execute').resolves({});
createSubscriptionStub = sinon.stub(stripeStub.subscriptions, 'create');
updateSubscriptionStub = sinon.stub(stripeStub.subscriptions, 'update');
deleteSubscriptionStub = sinon.stub(stripeStub.subscriptions, 'del');
});

afterEach(() => {
getPricesStub.reset();
updateOrgStub.reset();
updateServiceLevelStub.reset();
createSubscriptionStub.reset();
updateSubscriptionStub.reset();
deleteSubscriptionStub.reset();
});

const createUseCase = () => {
const useCase = new UpsertSubscription(stripeStub as any, repo, { execute: getPricesStub } as any);
const useCase = new UpsertSubscription(
stripeStub as any,
{ execute: updateServiceLevelStub } as any,
{ execute: getPricesStub } as any
);

return useCase;
};
Expand Down Expand Up @@ -606,7 +612,6 @@ describe('UpsertSubscription', () => {
describe('Organization entity update', () => {
it('should update the organization with the new apiServiceLevel', async () => {
const useCase = createUseCase();
const customer = { ...mockCustomerBase, subscriptions: { data: [{}, {}] } };

await useCase.execute(
UpsertSubscriptionCommand.create({
Expand All @@ -616,9 +621,8 @@ describe('UpsertSubscription', () => {
})
);

expect(updateOrgStub.lastCall.args).to.deep.equal([
{ _id: 'organization_id' },
{ $set: { apiServiceLevel: ApiServiceLevelEnum.BUSINESS } },
expect(updateServiceLevelStub.lastCall.args).to.deep.equal([
{ organizationId: 'organization_id', apiServiceLevel: ApiServiceLevelEnum.BUSINESS, isTrial: false },
]);
});
});
Expand Down
47 changes: 26 additions & 21 deletions apps/api/src/app/testing/billing/webhook.e2e-ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,20 @@ describe('Stripe webhooks', () => {
throw new Error('ee-billing does not exist');
}

const { SetupIntentSucceededHandler, CustomerSubscriptionCreatedHandler, UpsertSubscription, VerifyCustomer } =
eeBilling;
const {
SetupIntentSucceededHandler,
CustomerSubscriptionCreatedHandler,
UpsertSubscription,
VerifyCustomer,
UpdateServiceLevel,
} = eeBilling;

describe('setup_intent.succeeded', () => {
let updateCustomerStub: sinon.SinonStub;

let verifyCustomerStub: sinon.SinonStub;
let upsertSubscriptionStub: sinon.SinonStub;
let getFeatureFlagStub: sinon.SinonStub;

const analyticsServiceStub = {
track: sinon.stub(),
};
Expand Down Expand Up @@ -199,10 +204,8 @@ describe('Stripe webhooks', () => {

describe('customer.subscription.created', () => {
let verifyCustomerStub: sinon.SinonStub;
const organizationRepositoryStub = {
update: sinon.stub().resolves({ matched: 1, modified: 1 }),
updateServiceLevel: sinon.stub().resolves({ matched: 1, modified: 1 }),
};
let updateServiceLevelStub: sinon.SinonStub;

const analyticsServiceStub = {
track: sinon.stub(),
upsertGroup: sinon.stub(),
Expand Down Expand Up @@ -305,18 +308,15 @@ describe('Stripe webhooks', () => {
},
organization: { _id: 'organization_id', apiServiceLevel: ApiServiceLevelEnum.FREE },
} as any);
});

afterEach(() => {
organizationRepositoryStub.updateServiceLevel.reset();
updateServiceLevelStub = sinon.stub(UpdateServiceLevel.prototype, 'execute').resolves({});
});

const createHandler = () => {
const handler = new CustomerSubscriptionCreatedHandler(
{ execute: verifyCustomerStub } as any,
organizationRepositoryStub,
analyticsServiceStub as any,
invalidateCacheServiceStub as any
invalidateCacheServiceStub as any,
{ execute: updateServiceLevelStub } as any
);

return handler;
Expand Down Expand Up @@ -346,8 +346,11 @@ describe('Stripe webhooks', () => {
const handler = createHandler();
await handler.handle(event);

expect(organizationRepositoryStub.updateServiceLevel.calledWith('organization_id', ApiServiceLevelEnum.BUSINESS))
.to.be.true;
expect(updateServiceLevelStub.lastCall.args.at(0)).to.deep.equal({
organizationId: 'organization_id',
apiServiceLevel: ApiServiceLevelEnum.BUSINESS,
isTrial: false,
});
});

it('should exit early with unknown organization', async () => {
Expand Down Expand Up @@ -379,7 +382,7 @@ describe('Stripe webhooks', () => {
const handler = createHandler();
await handler.handle(event);

expect(organizationRepositoryStub.updateServiceLevel.called).to.be.false;
expect(updateServiceLevelStub.called).to.be.false;
});

it('should handle event with known organization and licensed subscription', async () => {
Expand Down Expand Up @@ -416,8 +419,11 @@ describe('Stripe webhooks', () => {
const handler = createHandler();
await handler.handle(event);

expect(organizationRepositoryStub.updateServiceLevel.calledWith('organization_id', ApiServiceLevelEnum.BUSINESS))
.to.be.true;
expect(updateServiceLevelStub.lastCall.args.at(0)).to.deep.equal({
organizationId: 'organization_id',
apiServiceLevel: ApiServiceLevelEnum.BUSINESS,
isTrial: false,
});
});

it('should invalidate the subscription cache with known organization and licensed subscription', async () => {
Expand Down Expand Up @@ -491,8 +497,7 @@ describe('Stripe webhooks', () => {
const handler = createHandler();
await handler.handle(event);

expect(organizationRepositoryStub.updateServiceLevel.calledWith('organization_id', ApiServiceLevelEnum.BUSINESS))
.to.be.true;
expect(updateServiceLevelStub.called).to.be.true;
});

it('should exit early with known organization and invalid apiServiceLevel', async () => {
Expand Down Expand Up @@ -566,7 +571,7 @@ describe('Stripe webhooks', () => {
const handler = createHandler();
await handler.handle(event);

expect(organizationRepositoryStub.updateServiceLevel.called).to.be.false;
expect(updateServiceLevelStub.called).to.be.false;
});
});
});
2 changes: 0 additions & 2 deletions enterprise/packages/auth/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
"strict": true,
"types": ["node", "mocha"],
"skipLibCheck": true,
"declaration": false,
"declarationMap": false,
"typeRoots": ["./node_modules/@types", "../../node_modules/@types"]
},
"include": ["src/**/*.ts"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { ApiServiceLevelEnum } from '@novu/shared';
import { IPartnerConfiguration, OrganizationDBModel, OrganizationEntity } from './organization.entity';
import { BaseRepository } from '../base-repository';
import { Organization } from './organization.schema';
import { CommunityMemberRepository } from '../member';
import { IOrganizationRepository } from './organization-repository.interface';
import { IntegrationRepository } from '../integration';

export class CommunityOrganizationRepository
extends BaseRepository<OrganizationDBModel, OrganizationEntity, object>
implements IOrganizationRepository
{
private memberRepository = new CommunityMemberRepository();
private integrationRepository = new IntegrationRepository();

constructor() {
super(Organization, OrganizationEntity);
Expand Down Expand Up @@ -64,23 +61,6 @@ export class CommunityOrganizationRepository
);
}

async updateServiceLevel(organizationId: string, apiServiceLevel: ApiServiceLevelEnum) {
if (apiServiceLevel === ApiServiceLevelEnum.FREE) {
await this.integrationRepository.setRemoveNovuBranding(organizationId, false);
}

return this.update(
{
_id: organizationId,
},
{
$set: {
apiServiceLevel,
},
}
);
}

async updateDefaultLocale(
organizationId: string,
defaultLocale: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ApiServiceLevelEnum } from '@novu/shared';
import { Types } from 'mongoose';
import { IPartnerConfiguration, OrganizationEntity } from './organization.entity';

Expand All @@ -19,13 +18,6 @@ export interface IOrganizationRepository extends IOrganizationRepositoryMongo {
matched: number;
modified: number;
}>;
updateServiceLevel(
organizationId: string,
apiServiceLevel: ApiServiceLevelEnum
): Promise<{
matched: number;
modified: number;
}>;
updateDefaultLocale(
organizationId: string,
defaultLocale: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export class OrganizationRepository implements IOrganizationRepository {
return this.organizationRepository.renameOrganization(organizationId, payload);
}

updateServiceLevel(organizationId: string, apiServiceLevel: ApiServiceLevelEnum) {
return this.organizationRepository.updateServiceLevel(organizationId, apiServiceLevel);
}

updateDefaultLocale(organizationId: string, defaultLocale: string): Promise<{ matched: number; modified: number }> {
return this.organizationRepository.updateDefaultLocale(organizationId, defaultLocale);
}
Expand Down
2 changes: 1 addition & 1 deletion libs/testing/src/ee/ee.organization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ export class EEOrganizationService {
}

async updateServiceLevel(organizationId: string, serviceLevel: ApiServiceLevelEnum) {
await this.organizationRepository.updateServiceLevel(organizationId, serviceLevel);
await this.communityOrganizationRepository.update({ _id: organizationId }, { apiServiceLevel: serviceLevel });
}
}
2 changes: 1 addition & 1 deletion libs/testing/src/organization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export class OrganizationService {
}

async updateServiceLevel(organizationId: string, serviceLevel: ApiServiceLevelEnum) {
await this.organizationRepository.updateServiceLevel(organizationId, serviceLevel);
await this.organizationRepository.update({ _id: organizationId }, { apiServiceLevel: serviceLevel });
}
}
Loading