Skip to content

Commit

Permalink
feat(api): apply member limit per billing plan (#6630)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChmaraX authored and tatarco committed Oct 7, 2024
1 parent a6ad712 commit 9132ffa
Show file tree
Hide file tree
Showing 9 changed files with 46 additions and 71 deletions.
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 });
}
}

0 comments on commit 9132ffa

Please sign in to comment.