Skip to content

Commit

Permalink
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
antonjoel82 authored Jul 18, 2024
2 parents c5fafde + a02b497 commit 8aa698f
Show file tree
Hide file tree
Showing 71 changed files with 772 additions and 198 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/dev-deploy-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ jobs:
test_api:
strategy:
matrix:
name: ['novu/api-ee', 'novu/api']
name: ['novu/api-ee', 'novu/api', 'novu/api-ee-clerk']
uses: ./.github/workflows/reusable-api-e2e.yml
with:
ee: ${{ contains (matrix.name,'-ee') }}
ee-clerk: ${{ contains (matrix.name,'-ee-clerk') }}
job-name: ${{ matrix.name }}
secrets: inherit

Expand Down
8 changes: 0 additions & 8 deletions apps/api/src/app/inbox/dtos/button-type-request.dto.ts

This file was deleted.

16 changes: 16 additions & 0 deletions apps/api/src/app/inbox/dtos/get-preferences-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IPreferenceChannels, IPreferenceOverride, ITemplateConfiguration, PreferenceLevelEnum } from '@novu/shared';
import { IsDefined, IsEnum, IsOptional } from 'class-validator';

export class GetPreferencesResponseDto {
@IsDefined()
@IsEnum({
enum: PreferenceLevelEnum,
})
level: PreferenceLevelEnum;

@IsOptional()
workflow?: ITemplateConfiguration;

@IsDefined()
preferences: { enabled: boolean; channels: IPreferenceChannels; overrides?: IPreferenceOverride[] };
}
55 changes: 55 additions & 0 deletions apps/api/src/app/inbox/e2e/get-preferences.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SubscriberRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { expect } from 'chai';

describe('Get all preferences - /inbox/preferences (GET)', function () {
let session: UserSession;
let subscriberRepository: SubscriberRepository;

beforeEach(async () => {
session = new UserSession();
subscriberRepository = new SubscriberRepository();
await session.initialize();
});

it('should always get the global preferences even if workflow preferences are not present', async function () {
const response = await session.testAgent
.get('/v1/inbox/preferences')
.set('Authorization', `Bearer ${session.subscriberToken}`);

const globalPreference = response.body.data[0];

expect(globalPreference.preferences.channels.email).to.equal(true);
expect(globalPreference.preferences.channels.in_app).to.equal(true);
expect(globalPreference.level).to.equal('global');
expect(response.body.data.length).to.equal(1);
});

it('should get both global and workflow preferences if workflow is present', async function () {
await session.createTemplate({
noFeedId: true,
});

const response = await session.testAgent
.get('/v1/inbox/preferences')
.set('Authorization', `Bearer ${session.subscriberToken}`);

const globalPreference = response.body.data[0];

expect(globalPreference.preferences.channels.email).to.equal(true);
expect(globalPreference.preferences.channels.in_app).to.equal(true);
expect(globalPreference.level).to.equal('global');

const workflowPreference = response.body.data[1];

expect(workflowPreference.preferences.channels.email).to.equal(true);
expect(workflowPreference.preferences.channels.in_app).to.equal(true);
expect(workflowPreference.level).to.equal('template');
});

it('should throw error when made unauthorized call', async function () {
const response = await session.testAgent.get(`/v1/inbox/preferences`).set('Authorization', `Bearer InvalidToken`);

expect(response.status).to.equal(401);
});
});
20 changes: 19 additions & 1 deletion apps/api/src/app/inbox/inbox.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import { UpdateNotificationActionCommand } from './usecases/update-notification-
import { UpdateAllNotificationsRequestDto } from './dtos/update-all-notifications-request.dto';
import { UpdateAllNotificationsCommand } from './usecases/update-all-notifications/update-all-notifications.command';
import { UpdateAllNotifications } from './usecases/update-all-notifications/update-all-notifications.usecase';
import { GetPreferences } from './usecases/get-preferences/get-preferences.usecase';
import { GetPreferencesCommand } from './usecases/get-preferences/get-preferences.command';
import { GetPreferencesResponseDto } from './dtos/get-preferences-response.dto';

@ApiCommonResponses()
@Controller('/inbox')
Expand All @@ -38,7 +41,8 @@ export class InboxController {
private notificationsCountUsecase: NotificationsCount,
private markNotificationAsUsecase: MarkNotificationAs,
private updateNotificationActionUsecase: UpdateNotificationAction,
private updateAllNotifications: UpdateAllNotifications
private updateAllNotifications: UpdateAllNotifications,
private getPreferencesUsecase: GetPreferences
) {}

@Post('/session')
Expand Down Expand Up @@ -94,6 +98,20 @@ export class InboxController {
return res;
}

@UseGuards(AuthGuard('subscriberJwt'))
@Get('/preferences')
async getAllPreferences(
@SubscriberSession() subscriberSession: SubscriberEntity
): Promise<GetPreferencesResponseDto[]> {
return await this.getPreferencesUsecase.execute(
GetPreferencesCommand.create({
organizationId: subscriberSession._organizationId,
subscriberId: subscriberSession.subscriberId,
environmentId: subscriberSession._environmentId,
})
);
}

@UseGuards(AuthGuard('subscriberJwt'))
@Patch('/notifications/:id/read')
async markNotificationAsRead(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { EnvironmentWithSubscriber } from '../../../shared/commands/project.command';

export class GetPreferencesCommand extends EnvironmentWithSubscriber {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
AnalyticsService,
GetSubscriberGlobalPreference,
GetSubscriberTemplatePreference,
} from '@novu/application-generic';
import { NotificationTemplateRepository, SubscriberRepository } from '@novu/dal';
import { PreferenceLevelEnum } from '@novu/shared';
import { expect } from 'chai';
import { sub } from 'date-fns';
import sinon from 'sinon';
import { AnalyticsEventsEnum } from '../../utils';
import { GetPreferences } from './get-preferences.usecase';

const mockedSubscriber: any = { _id: '123', subscriberId: 'test-mockSubscriber', firstName: 'test', lastName: 'test' };
const mockedWorkflowPreference: any = {
template: {},
preference: {
enabled: true,
channels: {
email: true,
in_app: true,
sms: false,
push: false,
chat: true,
},
overrides: {
email: false,
in_app: false,
sms: true,
push: true,
chat: false,
},
},
};

const mockedGlobalPreferences: any = {
preference: {
enabled: true,
channels: {
email: true,
in_app: true,
sms: false,
push: false,
chat: true,
},
},
};
const mockedPreferencesResponse: any = [
{ level: PreferenceLevelEnum.GLOBAL, preferences: mockedGlobalPreferences.preference },
{
level: PreferenceLevelEnum.TEMPLATE,
preferences: mockedWorkflowPreference.preference,
workflow: mockedWorkflowPreference.template,
},
];

const mockedWorkflow: any = [{}];

describe('GetPreferences', () => {
let getPreferences: GetPreferences;
let subscriberRepositoryMock: sinon.SinonStubbedInstance<SubscriberRepository>;
let getSubscriberWorkflowMock: sinon.SinonStubbedInstance<GetSubscriberTemplatePreference>;
let analyticsServiceMock: sinon.SinonStubbedInstance<AnalyticsService>;
let getSubscriberGlobalPreferenceMock: sinon.SinonStubbedInstance<GetSubscriberGlobalPreference>;
let notificationTemplateRepositoryMock: sinon.SinonStubbedInstance<NotificationTemplateRepository>;

beforeEach(() => {
subscriberRepositoryMock = sinon.createStubInstance(SubscriberRepository);
getSubscriberWorkflowMock = sinon.createStubInstance(GetSubscriberTemplatePreference);
analyticsServiceMock = sinon.createStubInstance(AnalyticsService);
getSubscriberGlobalPreferenceMock = sinon.createStubInstance(GetSubscriberGlobalPreference);
notificationTemplateRepositoryMock = sinon.createStubInstance(NotificationTemplateRepository);

getPreferences = new GetPreferences(
subscriberRepositoryMock as any,
notificationTemplateRepositoryMock as any,
getSubscriberWorkflowMock as any,
getSubscriberGlobalPreferenceMock as any,
analyticsServiceMock as any
);
});

afterEach(() => {
sinon.restore();
});

it('it should throw exception when subscriber is not found', async () => {
const command = {
environmentId: 'env-1',
organizationId: 'org-1',
subscriberId: 'not-found',
};

subscriberRepositoryMock.findOne.resolves(undefined);

try {
await getPreferences.execute(command);
} catch (error) {
expect(error).to.be.instanceOf(Error);
expect(error.message).to.equal(`Subscriber with id: ${command.subscriberId} not found`);
}
});

it('it should return subscriber preferences', async () => {
const command = {
environmentId: 'env-1',
organizationId: 'org-1',
subscriberId: 'test-mockSubscriber',
};

subscriberRepositoryMock.findBySubscriberId.resolves(mockedSubscriber);
getSubscriberGlobalPreferenceMock.execute.resolves(mockedGlobalPreferences);
notificationTemplateRepositoryMock.getActiveList.resolves(mockedWorkflow);
getSubscriberWorkflowMock.execute.resolves(mockedWorkflowPreference);

const result = await getPreferences.execute(command);

expect(subscriberRepositoryMock.findBySubscriberId.calledOnce).to.be.true;
expect(subscriberRepositoryMock.findBySubscriberId.firstCall.args).to.deep.equal([
command.environmentId,
command.subscriberId,
]);
expect(getSubscriberGlobalPreferenceMock.execute.calledOnce).to.be.true;
expect(getSubscriberGlobalPreferenceMock.execute.firstCall.args).to.deep.equal([command]);
expect(notificationTemplateRepositoryMock.getActiveList.calledOnce).to.be.true;
expect(notificationTemplateRepositoryMock.getActiveList.firstCall.args).to.deep.equal([
command.organizationId,
command.environmentId,
true,
]);
expect(getSubscriberWorkflowMock.execute.calledOnce).to.be.true;
expect(getSubscriberWorkflowMock.execute.firstCall.args).to.deep.equal([
{
organizationId: command.organizationId,
subscriberId: command.subscriberId,
environmentId: command.environmentId,
template: mockedWorkflow[0],
subscriber: mockedSubscriber,
},
]);

expect(analyticsServiceMock.mixpanelTrack.calledOnce).to.be.true;
expect(analyticsServiceMock.mixpanelTrack.firstCall.args).to.deep.equal([
AnalyticsEventsEnum.FETCH_PREFERENCES,
'',
{
_organization: command.organizationId,
subscriberId: command.subscriberId,
workflowSize: 1,
},
]);

expect(result).to.deep.equal(mockedPreferencesResponse);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
AnalyticsService,
GetSubscriberGlobalPreference,
GetSubscriberGlobalPreferenceCommand,
GetSubscriberTemplatePreference,
GetSubscriberTemplatePreferenceCommand,
} from '@novu/application-generic';
import { NotificationTemplateRepository, SubscriberRepository } from '@novu/dal';
import { ISubscriberPreferences, PreferenceLevelEnum } from '@novu/shared';
import { AnalyticsEventsEnum } from '../../utils';
import { GetPreferencesCommand } from './get-preferences.command';

@Injectable()
export class GetPreferences {
constructor(
private subscriberRepository: SubscriberRepository,
private notificationTemplateRepository: NotificationTemplateRepository,
private getSubscriberTemplatePreferenceUsecase: GetSubscriberTemplatePreference,
private getSubscriberGlobalPreference: GetSubscriberGlobalPreference,
private analyticsService: AnalyticsService
) {}

async execute(command: GetPreferencesCommand): Promise<ISubscriberPreferences[]> {
const subscriber = await this.subscriberRepository.findBySubscriberId(command.environmentId, command.subscriberId);

if (!subscriber) {
throw new NotFoundException(`Subscriber with id: ${command.subscriberId} not found`);
}

const globalPreference = await this.getSubscriberGlobalPreference.execute(
GetSubscriberGlobalPreferenceCommand.create({
organizationId: command.organizationId,
environmentId: command.environmentId,
subscriberId: command.subscriberId,
})
);

const updatedGlobalPreference = {
level: PreferenceLevelEnum.GLOBAL,
preferences: globalPreference.preference,
};

const workflowList =
(await this.notificationTemplateRepository.getActiveList(command.organizationId, command.environmentId, true)) ||
[];

this.analyticsService.mixpanelTrack(AnalyticsEventsEnum.FETCH_PREFERENCES, '', {
_organization: command.organizationId,
subscriberId: command.subscriberId,
workflowSize: workflowList.length,
});

const workflowPreferences = await Promise.all(
workflowList.map(async (workflow) => {
const workflowPreference = await this.getSubscriberTemplatePreferenceUsecase.execute(
GetSubscriberTemplatePreferenceCommand.create({
organizationId: command.organizationId,
subscriberId: command.subscriberId,
environmentId: command.environmentId,
template: workflow,
subscriber,
})
);

return {
level: PreferenceLevelEnum.TEMPLATE,
preferences: workflowPreference.preference,
workflow: workflowPreference.template,
};
})
);

return [updatedGlobalPreference, ...workflowPreferences];
}
}
11 changes: 8 additions & 3 deletions apps/api/src/app/inbox/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Session } from './session/session.usecase';
import { NotificationsCount } from './notifications-count/notifications-count.usecase';
import { GetSubscriberGlobalPreference, GetSubscriberTemplatePreference } from '@novu/application-generic';
import { GetNotifications } from './get-notifications/get-notifications.usecase';
import { GetPreferences } from './get-preferences/get-preferences.usecase';
import { MarkManyNotificationsAs } from './mark-many-notifications-as/mark-many-notifications-as.usecase';
import { MarkNotificationAs } from './mark-notification-as/mark-notification-as.usecase';
import { UpdateNotificationAction } from './update-notification-action/update-notification-action.usecase';
import { NotificationsCount } from './notifications-count/notifications-count.usecase';
import { Session } from './session/session.usecase';
import { UpdateAllNotifications } from './update-all-notifications/update-all-notifications.usecase';
import { UpdateNotificationAction } from './update-notification-action/update-notification-action.usecase';

export const USE_CASES = [
Session,
Expand All @@ -14,4 +16,7 @@ export const USE_CASES = [
MarkNotificationAs,
UpdateNotificationAction,
UpdateAllNotifications,
GetPreferences,
GetSubscriberGlobalPreference,
GetSubscriberTemplatePreference,
];
Loading

0 comments on commit 8aa698f

Please sign in to comment.