Skip to content

Commit

Permalink
feat: adding project category restriction checks while getting allowe…
Browse files Browse the repository at this point in the history
…d categories for project (#3177)

* feat: adding project category restriction checks while getting allowed categories for project - part 1 (#3177)

* feat: adding project category restriction checks while setting split expense values based on project - part 2 (#3179)

* test: fixing test coverage for project category restriction checks (#3180)

* fix: mileage and per diem forms to show all projects if project-category restriction is disabled (#3181)
  • Loading branch information
Aniruddha-Shriwant authored Aug 27, 2024
1 parent d048d07 commit e0be093
Show file tree
Hide file tree
Showing 22 changed files with 447 additions and 150 deletions.
10 changes: 10 additions & 0 deletions src/app/core/mock-data/org-settings.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1423,3 +1423,13 @@ export const orgSettingsWithCommuteDeductionsDisabled: OrgSettings = deepFreeze(
allowed: false,
},
});

export const orgSettingsWithProjectCategoryRestrictions: OrgSettings = deepFreeze({
...orgSettingsData,
advanced_projects: {
allowed: true,
enabled: true,
enable_individual_projects: true,
enable_category_restriction: true,
},
});
6 changes: 6 additions & 0 deletions src/app/core/models/org-settings.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// TODO: Clean up the file as per the rules mentioned in the eslint file:

/* eslint-disable custom-rules/one-enum-per-file */
/* eslint-disable custom-rules/prefer-semantic-extension-name */
/* eslint-disable custom-rules/one-interface-per-file */
import { AllowedPaymentModes } from './allowed-payment-modes.enum';
import { MileageDetails } from './mileage.model';
import { TaxGroup } from './tax-group.model';
Expand Down Expand Up @@ -180,6 +185,7 @@ export interface OrgMileageSettings extends CommonOrgSettings {

export interface AdvancedProjectSettings extends CommonOrgSettings {
enable_individual_projects?: boolean;
enable_category_restriction?: boolean;
}

export interface SSOIntegrationSettings extends CommonOrgSettings {
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/services/org-settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export class OrgSettingsService {
enabled: incoming.advanced_project_settings && incoming.advanced_project_settings.enabled,
enable_individual_projects:
incoming.advanced_project_settings && incoming.advanced_project_settings.enable_individual_projects,
enable_category_restriction:
incoming.advanced_project_settings && incoming.advanced_project_settings.enable_category_restriction,
},
advance_requests: {
allowed: incoming.advances_settings && incoming.advances_settings.allowed,
Expand Down Expand Up @@ -443,6 +445,7 @@ export class OrgSettingsService {
allowed: outgoing.advanced_projects.allowed,
enabled: outgoing.advanced_projects.enabled,
enable_individual_projects: outgoing.advanced_projects.enable_individual_projects,
enable_category_restriction: outgoing.advanced_projects.enable_category_restriction,
},
org_cost_center_settings: {
allowed: outgoing.cost_centers.allowed,
Expand Down
43 changes: 34 additions & 9 deletions src/app/core/services/projects.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
platformAPIResponseNullCategories,
} from '../mock-data/platform/v1/platform-project.data';
import { ProjectPlatformParams } from '../mock-data/platform/v1/platform-projects-params.data';
import { cloneDeep } from 'lodash';

const fixDate = (data) =>
data.map((datum) => ({
Expand Down Expand Up @@ -189,7 +190,7 @@ describe('ProjectsService', () => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);

projectsService.getByParamsUnformatted({}).subscribe((res) => {
projectsService.getByParamsUnformatted({}, true).subscribe((res) => {
expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(
platformAPIResponseMultiple.data,
undefined
Expand All @@ -203,7 +204,7 @@ describe('ProjectsService', () => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);

projectsService.getByParamsUnformatted({}, testActiveCategoryList).subscribe((res) => {
projectsService.getByParamsUnformatted({}, true, testActiveCategoryList).subscribe((res) => {
expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(
platformAPIResponseMultiple.data,
testActiveCategoryList
Expand All @@ -217,7 +218,7 @@ describe('ProjectsService', () => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);

projectsService.getByParamsUnformatted({}, null).subscribe((res) => {
projectsService.getByParamsUnformatted({}, true, null).subscribe((res) => {
expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(platformAPIResponseMultiple.data, null);
expect(res).toEqual(fixDate(apiV2ResponseMultiple.data));
done();
Expand All @@ -228,7 +229,7 @@ describe('ProjectsService', () => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);

projectsService.getByParamsUnformatted(testProjectParams, null).subscribe((res) => {
projectsService.getByParamsUnformatted(testProjectParams, true, null).subscribe((res) => {
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', {
params: ProjectPlatformParams,
});
Expand All @@ -242,7 +243,7 @@ describe('ProjectsService', () => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);

projectsService.getByParamsUnformatted(testProjectParams).subscribe((res) => {
projectsService.getByParamsUnformatted(testProjectParams, true).subscribe((res) => {
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', {
params: ProjectPlatformParams,
});
Expand All @@ -259,7 +260,7 @@ describe('ProjectsService', () => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);

projectsService.getByParamsUnformatted(testProjectParams, testActiveCategoryList).subscribe((res) => {
projectsService.getByParamsUnformatted(testProjectParams, true, testActiveCategoryList).subscribe((res) => {
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', {
params: ProjectPlatformParams,
});
Expand All @@ -271,16 +272,40 @@ describe('ProjectsService', () => {
done();
});
});

it('should not pass any or param of category_ids when activeCategoryList are provided and isProjectCategoryRestrictionsEnabled is false', (done) => {
spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple));
spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse);
const expectedParams = cloneDeep(ProjectPlatformParams);
delete expectedParams.or;

projectsService.getByParamsUnformatted(testProjectParams, false, testActiveCategoryList).subscribe((res) => {
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', {
params: expectedParams,
});
expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(
platformAPIResponseMultiple.data,
testActiveCategoryList
);
expect(res).toEqual(expectedProjectsResponse);
done();
});
});
});

describe('getAllowedOrgCategoryIds():', () => {
it('should category list after filter as per project passed', () => {
const result = projectsService.getAllowedOrgCategoryIds(testProjectV2, testActiveCategoryList);
it('should return category list after filter as per project passed and if isProjectCategoryRestrictionsEnabled is true', () => {
const result = projectsService.getAllowedOrgCategoryIds(testProjectV2, testActiveCategoryList, true);
expect(result).toEqual(allowedActiveCategories);
});

it('should return whole category list if project passed is restricted but isProjectCategoryRestrictionsEnabled is false', () => {
const result = projectsService.getAllowedOrgCategoryIds(testProjectV2, testActiveCategoryList, false);
expect(result).toEqual(testActiveCategoryList);
});

it('should return whole category list if project passed is not present', () => {
const result = projectsService.getAllowedOrgCategoryIds(null, testActiveCategoryList);
const result = projectsService.getAllowedOrgCategoryIds(null, testActiveCategoryList, true);
expect(result).toEqual(testActiveCategoryList);
});
});
Expand Down
19 changes: 14 additions & 5 deletions src/app/core/services/projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class ProjectsService {
@Cacheable()
getByParamsUnformatted(
projectParams: PlatformProjectArgs,
isProjectCategoryRestrictionsEnabled: boolean,
activeCategoryList?: OrgCategory[]
): Observable<ProjectV2[]> {
// eslint-disable-next-line prefer-const
Expand All @@ -46,7 +47,7 @@ export class ProjectsService {
this.addActiveFilter(isEnabled, params);

// `orgCategoryIds` can be optional
this.addOrgCategoryIdsFilter(orgCategoryIds, params);
this.addOrgCategoryIdsFilter(orgCategoryIds, params, isProjectCategoryRestrictionsEnabled);

// `searchNameText` can be optional
this.addNameSearchFilter(searchNameText, params);
Expand Down Expand Up @@ -93,8 +94,12 @@ export class ProjectsService {
}
}

addOrgCategoryIdsFilter(orgCategoryIds: string[], params: PlatformProjectParams): void {
if (typeof orgCategoryIds !== 'undefined' && orgCategoryIds !== null) {
addOrgCategoryIdsFilter(
orgCategoryIds: string[],
params: PlatformProjectParams,
isProjectCategoryRestrictionsEnabled: boolean
): void {
if (typeof orgCategoryIds !== 'undefined' && orgCategoryIds !== null && isProjectCategoryRestrictionsEnabled) {
params.or = '(category_ids.is.null, ' + 'category_ids.ov.{' + orgCategoryIds.join(',') + '}' + ')';
}
}
Expand All @@ -105,9 +110,13 @@ export class ProjectsService {
}
}

getAllowedOrgCategoryIds(project: ProjectParams | ProjectV2, activeCategoryList: OrgCategory[]): OrgCategory[] {
getAllowedOrgCategoryIds(
project: ProjectParams | ProjectV2,
activeCategoryList: OrgCategory[],
isProjectCategoryRestrictionsEnabled: boolean
): OrgCategory[] {
let categoryList: OrgCategory[] = [];
if (project) {
if (project && isProjectCategoryRestrictionsEnabled) {
categoryList = activeCategoryList.filter((category: OrgCategory) => {
const catId = category.id;
return project.project_org_category_ids.indexOf(catId as never) > -1;
Expand Down
3 changes: 3 additions & 0 deletions src/app/core/services/recently-used-items.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,14 @@ describe('RecentlyUsedItemsService', () => {
recentValues: recentlyUsedRes,
eou: apiEouRes,
categoryIds: ['16558', '16559', '16560', '16561', '16562'],
isProjectCategoryRestrictionsEnabled: true,
activeCategoryList: testActiveCategoryList,
};

recentlyUsedItemsService.getRecentlyUsedProjects(config).subscribe((res) => {
expect(projectsService.getByParamsUnformatted).toHaveBeenCalledOnceWith(
platformProjectsArgs1,
true,
testActiveCategoryList
);
expect(res).toEqual(recentlyUsedProjectRes);
Expand All @@ -93,6 +95,7 @@ describe('RecentlyUsedItemsService', () => {
recentValues: null,
eou: apiEouRes,
categoryIds: ['16558', '16559', '16560', '16561', '16562'],
isProjectCategoryRestrictionsEnabled: true,
activeCategoryList: testActiveCategoryList,
};
recentlyUsedItemsService.getRecentlyUsedProjects(config).subscribe((res) => {
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/services/recently-used-items.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class RecentlyUsedItemsService {
recentValues: RecentlyUsed;
eou: ExtendedOrgUser;
categoryIds: string[];
isProjectCategoryRestrictionsEnabled: boolean;
activeCategoryList?: OrgCategory[];
}): Observable<ProjectV2[]> {
if (
Expand All @@ -43,6 +44,7 @@ export class RecentlyUsedItemsService {
offset: 0,
limit: 10,
},
config.isProjectCategoryRestrictionsEnabled,
config.activeCategoryList
)
.pipe(
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/test-data/org-settings.service.spec.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const orgSettingsGetData: OrgSettings = deepFreeze({
allowed: true,
enabled: true,
enable_individual_projects: true,
enable_category_restriction: true,
},
advance_requests: {
allowed: true,
Expand Down Expand Up @@ -457,6 +458,7 @@ export const orgSettingsPostData: OrgSettingsResponse = deepFreeze({
allowed: true,
enabled: true,
enable_individual_projects: true,
enable_category_restriction: true,
},
org_cost_center_settings: {
allowed: true,
Expand Down
69 changes: 65 additions & 4 deletions src/app/fyle/add-edit-expense/add-edit-expense-5.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
taxSettingsData,
taxSettingsData2,
orgSettingsParamsWithAdvanceWallet,
orgSettingsWithProjectCategoryRestrictions,
} from 'src/app/core/mock-data/org-settings.data';
import {
orgUserSettingsData,
Expand Down Expand Up @@ -466,6 +467,8 @@ export function TestCases5(getTestBed) {
});

describe('setupFilteredCategories():', () => {
beforeEach(() => (component.isProjectCategoryRestrictionsEnabled$ = of(true)));

it('should get filtered categories for a project', fakeAsync(() => {
component.etxn$ = of(unflattenedTxnData);
component.activeCategories$ = of(sortedCategory);
Expand All @@ -481,7 +484,11 @@ export function TestCases5(getTestBed) {

expect(component.fg.controls.billable.value).toBeFalse();
expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id, sortedCategory);
expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(apiV2ResponseMultiple[1], sortedCategory);
expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(
apiV2ResponseMultiple[1],
sortedCategory,
true
);
}));

it('should get updated filtered categories for changing an existing project', fakeAsync(() => {
Expand All @@ -500,7 +507,11 @@ export function TestCases5(getTestBed) {

expect(projectsService.getbyId).toHaveBeenCalledOnceWith(257528, sortedCategory);
expect(component.fg.controls.billable.value).toBeFalse();
expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(apiV2ResponseMultiple[1], sortedCategory);
expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(
apiV2ResponseMultiple[1],
sortedCategory,
true
);
}));

it('should return null the expense does not have project id', fakeAsync(() => {
Expand All @@ -516,7 +527,55 @@ export function TestCases5(getTestBed) {
tick(500);

expect(component.fg.controls.billable.value).toBeFalse();
expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(null, sortedCategory);
expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(null, sortedCategory, true);
}));

it('should filter recentCategories based on project_org_category_ids when restrictions are enabled', fakeAsync(() => {
component.isProjectCategoryRestrictionsEnabled$ = of(true);
component.etxn$ = of(unflattenedTxnData);
component.activeCategories$ = of(sortedCategory);
component.recentCategoriesOriginal = recentUsedCategoriesRes;
projectsService.getbyId.and.returnValue(of(apiV2ResponseMultiple[0]));
projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories);

const projectWithRestrictions = {
project_org_category_ids: [89469, 16576],
};

const expectedRecentCategories = recentUsedCategoriesRes.filter((category) =>
projectWithRestrictions.project_org_category_ids.includes(category.value.id)
);

component.setupFilteredCategories();
tick(500);

component.fg.controls.project.setValue(projectWithRestrictions);
fixture.detectChanges();
tick(500);

expect(component.recentCategories).toEqual(expectedRecentCategories);
}));

it('should set recentCategories to undefined if recentCategoriesOriginal is not present when restrictions are enabled', fakeAsync(() => {
component.isProjectCategoryRestrictionsEnabled$ = of(true);
component.etxn$ = of(unflattenedTxnData);
component.activeCategories$ = of(sortedCategory);
component.recentCategoriesOriginal = null;
projectsService.getbyId.and.returnValue(of(apiV2ResponseMultiple[0]));
projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories);

const projectWithRestrictions = {
project_org_category_ids: [89469, 16576],
};

component.setupFilteredCategories();
tick(500);

component.fg.controls.project.setValue(projectWithRestrictions);
fixture.detectChanges();
tick(500);

expect(component.recentCategories).toBeUndefined();
}));
});

Expand Down Expand Up @@ -754,6 +813,7 @@ export function TestCases5(getTestBed) {
it('getRecentProjects(): should get recent projects', (done) => {
component.activeCategories$ = of(sortedCategory);
component.recentlyUsedValues$ = of(recentlyUsedRes);
component.isProjectCategoryRestrictionsEnabled$ = of(true);
authService.getEou.and.resolveTo(apiEouRes);
component.fg.controls.category.setValue(orgCategoryData);
recentlyUsedItemsService.getRecentlyUsedProjects.and.returnValue(of(recentlyUsedProjectRes));
Expand All @@ -766,6 +826,7 @@ export function TestCases5(getTestBed) {
recentValues: recentlyUsedRes,
eou: apiEouRes,
categoryIds: component.fg.controls.category.value && component.fg.controls.category.value.id,
isProjectCategoryRestrictionsEnabled: true,
activeCategoryList: sortedCategory,
});
done();
Expand Down Expand Up @@ -1085,7 +1146,7 @@ export function TestCases5(getTestBed) {
spyOn(component, 'getSelectedCostCenters').and.returnValue(of(costCentersData[0]));
spyOn(component, 'getReceiptCount').and.returnValue(of(1));
currencyService.getHomeCurrency.and.returnValue(of('USD'));
orgSettingsService.get.and.returnValue(of(orgSettingsData));
orgSettingsService.get.and.returnValue(of(orgSettingsWithProjectCategoryRestrictions));
customInputsService.getAll.and.returnValue(of(expenseFieldResponse));
loaderService.hideLoader.and.resolveTo();
loaderService.showLoader.and.resolveTo();
Expand Down
Loading

0 comments on commit e0be093

Please sign in to comment.