diff --git a/src/app/core/mock-data/org-category.data.ts b/src/app/core/mock-data/org-category.data.ts index b683f921ab..379f7728e6 100644 --- a/src/app/core/mock-data/org-category.data.ts +++ b/src/app/core/mock-data/org-category.data.ts @@ -370,6 +370,8 @@ export const orgCategoryPaginated1: OrgCategory[] = deepFreeze([ }, ]); +export const categoryIds: string[] = deepFreeze(['129140']); + export const orgCategoryPaginated2: OrgCategory[] = deepFreeze([ { code: '51708', diff --git a/src/app/core/mock-data/platform/v1/platform-project-args.data.ts b/src/app/core/mock-data/platform/v1/platform-project-args.data.ts new file mode 100644 index 0000000000..82d4c86b27 --- /dev/null +++ b/src/app/core/mock-data/platform/v1/platform-project-args.data.ts @@ -0,0 +1,15 @@ +import { PlatformProjectArgs } from 'src/app/core/models/platform/v1/platform-project-args.model'; +import deepFreeze from 'deep-freeze-strict'; +import { apiEouRes } from '../../extended-org-user.data'; +import { recentlyUsedRes } from '../../recently-used.data'; + +export const platformProjectsArgs1: PlatformProjectArgs = deepFreeze({ + orgId: apiEouRes.ou.org_id, + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: ['16558', '16559', '16560', '16561', '16562'], + projectIds: recentlyUsedRes.recent_project_ids, + offset: 0, + limit: 10, +}); diff --git a/src/app/core/mock-data/platform/v1/platform-project.data.ts b/src/app/core/mock-data/platform/v1/platform-project.data.ts new file mode 100644 index 0000000000..4a621fb0ea --- /dev/null +++ b/src/app/core/mock-data/platform/v1/platform-project.data.ts @@ -0,0 +1,122 @@ +import { PlatformApiResponse } from '../../../models/platform/platform-api-response.model'; +import { PlatformProject } from '../../../models/platform/platform-project.model'; +import deepFreeze from 'deep-freeze-strict'; + +export const platformProjectSingleRes: PlatformApiResponse = deepFreeze({ + count: 1, + data: [ + { + is_enabled: true, + code: '1184', + created_at: new Date('2021-05-12T10:28:40.834844'), + description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', + id: 257528, + name: 'Customer Mapped Project', + category_ids: [122269, 122270, 122271, null], + org_id: 'orFdTTTNcyye', + updated_at: new Date('2021-07-08T10:28:27.686886'), + display_name: 'Customer Mapped Project', + sub_project: null, + }, + ], + offset: 0, +}); + +export const platformAPIResponseMultiple: PlatformApiResponse = deepFreeze({ + count: 2, + data: [ + { + is_enabled: true, + code: '1184', + created_at: new Date('2021-05-12T10:28:40.834844'), + description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', + id: 257528, + display_name: 'Customer Mapped Project', + category_ids: [122269, 122270, 122271, null], + org_id: 'orFdTTTNcyye', + updated_at: new Date('2021-07-08T10:28:27.686886'), + name: 'Customer Mapped Project', + sub_project: null, + }, + { + is_enabled: true, + code: '1182', + created_at: new Date('2021-05-12T10:28:40.834844'), + description: 'Sage Intacct Project - Fyle Engineering, Id - 1182', + id: 257529, + display_name: 'Fyle Engineering', + category_ids: [122269, 122270, 122271], + org_id: 'orFdTTTNcyye', + updated_at: new Date('2021-07-08T10:28:27.686886'), + name: 'Fyle Engineering', + sub_project: null, + }, + ], + offset: 0, +}); + +export const platformAPIResponseActiveOnly: PlatformApiResponse = deepFreeze({ + count: 4, + data: [ + { + id: 257528, + created_at: new Date('2021-05-12T10:28:40.834Z'), + updated_at: new Date('2021-07-08T10:28:27.686Z'), + name: 'Customer Mapped Project', + sub_project: null, + code: '1184', + org_id: 'orFdTTTNcyye', + description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', + is_enabled: true, + category_ids: [null, 145429, 122269, 122271], + display_name: 'Customer Mapped Project', + }, + { + id: 257541, + created_at: new Date('2021-05-12T10:28:40.834Z'), + updated_at: new Date('2021-07-08T10:28:27.686Z'), + name: 'Sage Project 8', + sub_project: null, + code: '1178', + org_id: 'orFdTTTNcyye', + description: 'Sage Intacct Project - Sage Project 8, Id - 1178', + is_enabled: true, + category_ids: [null, 145429, 122269, 122271], + display_name: 'Customer Mapped Project', + }, + { + id: 257531, + created_at: new Date('2021-05-12T10:28:40.834Z'), + updated_at: new Date('2021-07-08T10:28:27.686Z'), + name: 'Fyle Team Integrations', + sub_project: null, + code: '1183', + org_id: 'orFdTTTNcyye', + description: 'Sage Intacct Project - Fyle Team Integrations, Id - 1183', + is_enabled: true, + category_ids: null, + display_name: 'Customer Mapped Project', + }, + ], + offset: 0, +}); + +export const platformAPIResponseNullCategories: PlatformApiResponse = deepFreeze({ + count: 4, + data: [ + { + id: 257528, + created_at: new Date('2021-05-12T10:28:40.834Z'), + updated_at: new Date('2021-07-08T10:28:27.686Z'), + name: 'Customer Mapped Project', + sub_project: null, + code: '1184', + org_id: 'orFdTTTNcyye', + description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', + is_enabled: true, + category_ids: null, + display_name: 'Customer Mapped Project', + }, + ], + offset: 0, +}); diff --git a/src/app/core/mock-data/platform/v1/platform-projects-params.data.ts b/src/app/core/mock-data/platform/v1/platform-projects-params.data.ts new file mode 100644 index 0000000000..f52d65ff55 --- /dev/null +++ b/src/app/core/mock-data/platform/v1/platform-projects-params.data.ts @@ -0,0 +1,13 @@ +import { PlatformProjectParams } from 'src/app/core/models/platform/v1/platform-project-params.model'; +import deepFreeze from 'deep-freeze-strict'; + +export const ProjectPlatformParams: PlatformProjectParams = deepFreeze({ + org_id: 'eq.orNVthTo2Zyo', + order: 'name.asc', + limit: 10, + offset: 0, + is_enabled: 'eq.true', + or: '(category_ids.is.null, category_ids.ov.{122269,122270,122271,122272,122273})', + id: 'in.(3943,305792,148971,247936)', + name: 'ilike.%search%', +}); diff --git a/src/app/core/models/platform/platform-project.model.ts b/src/app/core/models/platform/platform-project.model.ts new file mode 100644 index 0000000000..e92b323356 --- /dev/null +++ b/src/app/core/models/platform/platform-project.model.ts @@ -0,0 +1,13 @@ +export interface PlatformProject { + id: number; + org_id: string; + created_at: Date; + updated_at: Date; + name: string; + sub_project: string; + code: string; + display_name: string; + description: string; + is_enabled: boolean; + category_ids: number[]; +} diff --git a/src/app/core/models/platform/v1/platform-project-args.model.ts b/src/app/core/models/platform/v1/platform-project-args.model.ts new file mode 100644 index 0000000000..95282f0b15 --- /dev/null +++ b/src/app/core/models/platform/v1/platform-project-args.model.ts @@ -0,0 +1,11 @@ +export interface PlatformProjectArgs { + orgId?: string; + isEnabled?: boolean; + orgCategoryIds?: string[]; + searchNameText?: string; + limit?: number; + offset?: number; + sortOrder?: string; + sortDirection?: string; + projectIds?: number[]; +} diff --git a/src/app/core/models/platform/v1/platform-project-params.model.ts b/src/app/core/models/platform/v1/platform-project-params.model.ts new file mode 100644 index 0000000000..e5d5d59684 --- /dev/null +++ b/src/app/core/models/platform/v1/platform-project-params.model.ts @@ -0,0 +1,14 @@ +export interface PlatformProjectParams { + limit: number; + offset: number; + order?: string; + sortDirection?: string; + sortOrder?: string; + searchNameText?: string; + is_enabled?: string; + id?: string; + category_ids?: string; + org_id?: string; + name?: string; + or?: string; +} diff --git a/src/app/core/models/v2/project-v2.model.ts b/src/app/core/models/v2/project-v2.model.ts index 59edb030b5..8af239ae40 100644 --- a/src/app/core/models/v2/project-v2.model.ts +++ b/src/app/core/models/v2/project-v2.model.ts @@ -1,11 +1,11 @@ export interface ProjectV2 { - ap1_email: string; - ap1_full_name: string; - ap2_email: string; - ap2_full_name: string; + ap1_email?: string; + ap1_full_name?: string; + ap2_email?: string; + ap2_full_name?: string; project_active: boolean; - project_approver1_id: string; - project_approver2_id: string; + project_approver1_id?: string; + project_approver2_id?: string; project_code: string; project_created_at: Date; project_description: string; diff --git a/src/app/core/services/projects.service.spec.ts b/src/app/core/services/projects.service.spec.ts index 3152ac6a44..7eec68050e 100644 --- a/src/app/core/services/projects.service.spec.ts +++ b/src/app/core/services/projects.service.spec.ts @@ -7,15 +7,24 @@ import { apiV2ResponseMultiple, expectedReponseActiveOnly, apiV2ResponseSingle, - testActiveCategoryList, allowedActiveCategories, expectedProjectsResponse, testProjectParams, testProjectV2, testCategoryIds, - params, + testActiveCategoryList, + projectsV1Data, + expectedV2WithAllCategories, } from '../test-data/projects.spec.data'; import { ProjectsService } from './projects.service'; +import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service'; +import { + platformAPIResponseMultiple, + platformProjectSingleRes, + platformAPIResponseActiveOnly, + platformAPIResponseNullCategories, +} from '../mock-data/platform/v1/platform-project.data'; +import { ProjectPlatformParams } from '../mock-data/platform/v1/platform-projects-params.data'; const fixDate = (data) => data.map((datum) => ({ @@ -26,12 +35,12 @@ const fixDate = (data) => describe('ProjectsService', () => { let projectsService: ProjectsService; - let apiService: jasmine.SpyObj; - let apiV2Service: jasmine.SpyObj; + let spenderPlatformV1ApiService: jasmine.SpyObj; beforeEach(() => { const apiServiceSpy = jasmine.createSpyObj('ApiService', ['get']); const apiv2ServiceSpy = jasmine.createSpyObj('ApiV2Service', ['get']); + const spenderPlatformApiServiceSpy = jasmine.createSpyObj('SpenderPlatformV1ApiService', ['get']); TestBed.configureTestingModule({ providers: [ @@ -44,100 +53,325 @@ describe('ProjectsService', () => { provide: ApiV2Service, useValue: apiv2ServiceSpy, }, + { + provide: SpenderPlatformV1ApiService, + useValue: spenderPlatformApiServiceSpy, + }, ], }); projectsService = TestBed.inject(ProjectsService); - apiService = TestBed.inject(ApiService) as jasmine.SpyObj; - apiV2Service = TestBed.inject(ApiV2Service) as jasmine.SpyObj; + spenderPlatformV1ApiService = TestBed.inject( + SpenderPlatformV1ApiService + ) as jasmine.SpyObj; }); it('should be created', () => { expect(projectsService).toBeTruthy(); }); - it('should be able to fetch project by id', (done) => { - apiV2Service.get.and.returnValue(of(apiV2ResponseSingle)); - projectsService.getbyId(257528).subscribe((res) => { - expect(res).toEqual(fixDate(apiV2ResponseSingle.data)[0]); - done(); + describe('getbyId():', () => { + it('should be able to fetch project by id with activeCategoryList', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformProjectSingleRes)); + + spyOn(projectsService, 'transformToV2Response').and.returnValue([apiV2ResponseSingle.data[0]]); + projectsService.getbyId(257528, testActiveCategoryList).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: { + id: 'eq.257528', + }, + }); + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith( + platformProjectSingleRes.data, + testActiveCategoryList + ); + expect(res).toEqual(apiV2ResponseSingle.data[0]); + done(); + }); + }); + + it('should be able to fetch project by id with null activeCategoryList', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformProjectSingleRes)); + + spyOn(projectsService, 'transformToV2Response').and.returnValue([apiV2ResponseSingle.data[0]]); + projectsService.getbyId(257528, null).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: { + id: 'eq.257528', + }, + }); + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(platformProjectSingleRes.data, null); + expect(res).toEqual(apiV2ResponseSingle.data[0]); + done(); + }); }); - expect(apiV2Service.get).toHaveBeenCalledWith('/projects', { - params: { - project_id: 'eq.257528', - }, + it('should be able to fetch project by id and activeCategoryList is not provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformProjectSingleRes)); + + spyOn(projectsService, 'transformToV2Response').and.returnValue([apiV2ResponseSingle.data[0]]); + projectsService.getbyId(257528).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: { + id: 'eq.257528', + }, + }); + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith( + platformProjectSingleRes.data, + undefined + ); + expect(res).toEqual(apiV2ResponseSingle.data[0]); + done(); + }); }); }); - it('should be able to fetch all active projects', (done) => { - apiService.get.and.returnValue(of(apiResponseActiveOnly)); - projectsService.getAllActive().subscribe((res) => { - expect(res).toEqual(expectedReponseActiveOnly); - done(); + describe('getAllActive():', () => { + it('should be able to fetch all active projects', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + spyOn(projectsService, 'transformToV1Response').and.returnValue(expectedReponseActiveOnly); + + projectsService.getAllActive(testActiveCategoryList).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: { + is_enabled: `eq.true`, + }, + }); + expect(projectsService.transformToV1Response).toHaveBeenCalledOnceWith( + platformAPIResponseActiveOnly.data, + testActiveCategoryList + ); + expect(res).toEqual(expectedReponseActiveOnly); + done(); + }); }); - expect(apiService.get).toHaveBeenCalledWith('/projects', { - params: { - active_only: true, - }, + it('should be able to fetch all active projects with null activeCategoryList', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + spyOn(projectsService, 'transformToV1Response').and.returnValue(expectedReponseActiveOnly); + + projectsService.getAllActive(null).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: { + is_enabled: `eq.true`, + }, + }); + expect(projectsService.transformToV1Response).toHaveBeenCalledOnceWith( + platformAPIResponseActiveOnly.data, + null + ); + expect(res).toEqual(expectedReponseActiveOnly); + done(); + }); }); - }); - it('should be able to fetch data when no params provided', (done) => { - apiV2Service.get.and.returnValue(of(apiV2ResponseMultiple)); + it('should be able to fetch all active projects with activeCategoryList not provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + spyOn(projectsService, 'transformToV1Response').and.returnValue(expectedReponseActiveOnly); - projectsService.getByParamsUnformatted({}).subscribe((res) => { - expect(res).toEqual(fixDate(apiV2ResponseMultiple.data)); - done(); + projectsService.getAllActive().subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: { + is_enabled: `eq.true`, + }, + }); + expect(projectsService.transformToV1Response).toHaveBeenCalledOnceWith( + platformAPIResponseActiveOnly.data, + undefined + ); + expect(res).toEqual(expectedReponseActiveOnly); + done(); + }); }); }); - it('should be able to fetch data when params are provided', (done) => { - apiV2Service.get.and.returnValue(of(apiV2ResponseMultiple)); + describe('getByParamsUnformatted():', () => { + it('should be able to fetch data when no params provided and activeCategoryList not provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple)); + spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse); - const result = projectsService.getByParamsUnformatted(testProjectParams); + projectsService.getByParamsUnformatted({}).subscribe((res) => { + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith( + platformAPIResponseMultiple.data, + undefined + ); + expect(res).toEqual(fixDate(apiV2ResponseMultiple.data)); + done(); + }); + }); + + it('should be able to fetch data when no params provided but activeCategoryList is provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple)); + spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse); - result.subscribe((res) => { - expect(res).toEqual(expectedProjectsResponse); - expect(apiV2Service.get).toHaveBeenCalledWith('/projects', { - params, + projectsService.getByParamsUnformatted({}, testActiveCategoryList).subscribe((res) => { + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith( + platformAPIResponseMultiple.data, + testActiveCategoryList + ); + expect(res).toEqual(fixDate(apiV2ResponseMultiple.data)); + done(); + }); + }); + + it('should be able to fetch the data when there no params are provided and activeCategoryList is null', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple)); + spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse); + + projectsService.getByParamsUnformatted({}, null).subscribe((res) => { + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(platformAPIResponseMultiple.data, null); + expect(res).toEqual(fixDate(apiV2ResponseMultiple.data)); + done(); + }); + }); + + it('should be able to fetch data when params are provided and null activeCategoryList is provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple)); + spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse); + + projectsService.getByParamsUnformatted(testProjectParams, null).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: ProjectPlatformParams, + }); + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith(platformAPIResponseMultiple.data, null); + expect(res).toEqual(expectedProjectsResponse); + done(); }); - done(); }); - }); - it('should category list after filter as per project passed', () => { - const result = projectsService.getAllowedOrgCategoryIds(testProjectV2, testActiveCategoryList); - expect(result).toEqual(allowedActiveCategories); + it('should be able to fetch data when params are provided and no activeCategoryList is provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple)); + spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse); + + projectsService.getByParamsUnformatted(testProjectParams).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: ProjectPlatformParams, + }); + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith( + platformAPIResponseMultiple.data, + undefined + ); + expect(res).toEqual(expectedProjectsResponse); + done(); + }); + }); + + it('should be able to fetch data when params and activeCategoryList are provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseMultiple)); + spyOn(projectsService, 'transformToV2Response').and.returnValue(expectedProjectsResponse); + + projectsService.getByParamsUnformatted(testProjectParams, testActiveCategoryList).subscribe((res) => { + expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/projects', { + params: ProjectPlatformParams, + }); + expect(projectsService.transformToV2Response).toHaveBeenCalledOnceWith( + platformAPIResponseMultiple.data, + testActiveCategoryList + ); + expect(res).toEqual(expectedProjectsResponse); + done(); + }); + }); }); - it('should return whole category list if project passed is not present', () => { - const result = projectsService.getAllowedOrgCategoryIds(null, testActiveCategoryList); - expect(result).toEqual(testActiveCategoryList); + describe('getAllowedOrgCategoryIds():', () => { + it('should category list after filter as per project passed', () => { + const result = projectsService.getAllowedOrgCategoryIds(testProjectV2, testActiveCategoryList); + expect(result).toEqual(allowedActiveCategories); + }); + + it('should return whole category list if project passed is not present', () => { + const result = projectsService.getAllowedOrgCategoryIds(null, testActiveCategoryList); + expect(result).toEqual(testActiveCategoryList); + }); }); - it('should get project count restricted by a set of category IDs', (done) => { - apiService.get.and.returnValue(of(apiResponseActiveOnly)); + describe('getProjectCount():', () => { + it('should get project count restricted by a set of category IDs and null activeCategoryList', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + + projectsService.getProjectCount({ categoryIds: testCategoryIds }, null).subscribe((res) => { + expect(res).toEqual(2); + done(); + }); + }); + + it('should get project count restricted by a set of category IDs and activeCategoryList not provided', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + + projectsService.getProjectCount({ categoryIds: testCategoryIds }).subscribe((res) => { + expect(res).toEqual(2); + done(); + }); + }); + + it('should get project count restricted by a set of category IDs and activeCategoryList', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + spyOn(projectsService, 'getAllActive').and.returnValue(of(expectedReponseActiveOnly)); - const result = projectsService.getProjectCount({ categoryIds: testCategoryIds }); - result.subscribe((res) => { - expect(res).toEqual(2); + projectsService.getProjectCount({ categoryIds: testCategoryIds }, testActiveCategoryList).subscribe((res) => { + expect(projectsService.getAllActive).toHaveBeenCalledOnceWith(testActiveCategoryList); + expect(res).toEqual(2); + done(); + }); + }); + + it('should get project count not restricted by a set of category IDs', (done) => { + spenderPlatformV1ApiService.get.and.returnValue(of(platformAPIResponseActiveOnly)); + spyOn(projectsService, 'getAllActive').and.returnValue(of(expectedReponseActiveOnly)); + + const resultWithOutParam = projectsService.getProjectCount(); + const resultWithParam = projectsService.getProjectCount({ categoryIds: null }); + + resultWithOutParam.subscribe((res) => { + expect(res).toEqual(apiResponseActiveOnly.length); + }); + resultWithParam.subscribe((res) => { + expect(res).toEqual(apiResponseActiveOnly.length); + }); + expect(projectsService.getAllActive).toHaveBeenCalledWith(undefined); done(); }); }); - it('should get project count not restricted by a set of category IDs', (done) => { - apiService.get.and.returnValue(of(apiResponseActiveOnly)); + describe('transformToV1Response():', () => { + it('should correctly transform platformProject to ProjectV1 with activeCategoryList provided', () => { + const result = projectsService.transformToV1Response(platformAPIResponseMultiple.data, testActiveCategoryList); + expect(result).toEqual(projectsV1Data); + }); - const resultWithOutParam = projectsService.getProjectCount(); - const resultWithParam = projectsService.getProjectCount({ categoryIds: null }); + it('should correctly transform platformProject to ProjectV1 with activeCategoryList is null', () => { + const result = projectsService.transformToV1Response(platformAPIResponseMultiple.data, null); + expect(result).toEqual(projectsV1Data); + }); - resultWithOutParam.subscribe((res) => { - expect(res).toEqual(apiResponseActiveOnly.length); + it('should correctly transform platformProject to ProjectV1 with activeCategoryList is not provided', () => { + const result = projectsService.transformToV1Response(platformAPIResponseMultiple.data); + expect(result).toEqual(projectsV1Data); }); - resultWithParam.subscribe((res) => { - expect(res).toEqual(apiResponseActiveOnly.length); + }); + + describe('transformToV2Response():', () => { + it('should correctly transform platformProject to ProjectV2 with activeCategoryList provided', () => { + const result = projectsService.transformToV2Response(platformAPIResponseMultiple.data, testActiveCategoryList); + expect(result).toEqual(expectedProjectsResponse); + }); + + it('should correctly transform platformProject to ProjectV2 with activeCategoryList is null', () => { + const result = projectsService.transformToV2Response(platformAPIResponseMultiple.data, null); + expect(result).toEqual(expectedProjectsResponse); + }); + + it('should correctly transform platformProject to ProjectV2 with activeCategoryList is not provided', () => { + const result = projectsService.transformToV2Response(platformAPIResponseMultiple.data); + expect(result).toEqual(expectedProjectsResponse); + }); + + it('should handle platformProject with category_ids as null', () => { + const result = projectsService.transformToV2Response( + platformAPIResponseNullCategories.data, + testActiveCategoryList + ); + expect(result).toEqual(expectedV2WithAllCategories); }); - done(); }); }); diff --git a/src/app/core/services/projects.service.ts b/src/app/core/services/projects.service.ts index 7b4b8d7081..42dad67abb 100644 --- a/src/app/core/services/projects.service.ts +++ b/src/app/core/services/projects.service.ts @@ -9,42 +9,41 @@ import { ProjectV1 } from '../models/v1/extended-project.model'; import { ProjectParams } from '../models/project-params.model'; import { intersection } from 'lodash'; import { OrgCategory } from '../models/v1/org-category.model'; - +import { PlatformProject } from '../models/platform/platform-project.model'; +import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service'; +import { PlatformApiResponse } from '../models/platform/platform-api-response.model'; +import { PlatformProjectParams } from '../models/platform/v1/platform-project-params.model'; +import { PlatformProjectArgs } from '../models/platform/v1/platform-project-args.model'; @Injectable({ providedIn: 'root', }) export class ProjectsService { - constructor(private apiService: ApiService, private apiV2Service: ApiV2Service) {} + constructor( + private apiService: ApiService, + private apiV2Service: ApiV2Service, + private spenderPlatformV1ApiService: SpenderPlatformV1ApiService + ) {} @Cacheable() getByParamsUnformatted( - projectParams: Partial<{ - orgId: string; - active: boolean; - orgCategoryIds: string[]; - searchNameText: string; - limit: number; - offset: number; - sortOrder: string; - sortDirection: string; - projectIds: number[]; - }> + projectParams: PlatformProjectArgs, + activeCategoryList?: OrgCategory[] ): Observable { // eslint-disable-next-line prefer-const - let { orgId, active, orgCategoryIds, searchNameText, limit, offset, sortOrder, sortDirection, projectIds } = + let { orgId, isEnabled, orgCategoryIds, searchNameText, limit, offset, sortOrder, sortDirection, projectIds } = projectParams; sortOrder = sortOrder || 'project_updated_at'; sortDirection = sortDirection || 'desc'; - const params: ProjectParams = { - project_org_id: 'eq.' + orgId, + const params: PlatformProjectParams = { + org_id: 'eq.' + orgId, order: sortOrder + '.' + sortDirection, limit: limit || 200, offset: offset || 0, }; // `active` can be optional - this.addActiveFilter(active, params); + this.addActiveFilter(isEnabled, params); // `orgCategoryIds` can be optional this.addOrgCategoryIdsFilter(orgCategoryIds, params); @@ -55,25 +54,20 @@ export class ProjectsService { // `searchNameText` can be optional this.addNameSearchFilter(searchNameText, params); - return this.apiV2Service - .get('/projects', { + return this.spenderPlatformV1ApiService + .get>('/projects', { params, }) - .pipe( - map((res) => - res.data.map((datum) => ({ - ...datum, - project_created_at: new Date(datum.project_created_at), - project_updated_at: new Date(datum.project_updated_at), - })) - ) - ); + .pipe(map((res) => this.transformToV2Response(res.data, activeCategoryList))); } @Cacheable() - getProjectCount(params: { categoryIds: string[] } = { categoryIds: [] }): Observable { + getProjectCount( + params: { categoryIds: string[] } = { categoryIds: [] }, + activeCategoryList?: OrgCategory[] + ): Observable { const categoryIds = params.categoryIds?.map((categoryId) => parseInt(categoryId, 10)); - return this.getAllActive().pipe( + return this.getAllActive(activeCategoryList).pipe( map((projects) => { const filterdProjects = projects.filter((project) => { if (categoryIds?.length) { @@ -87,27 +81,27 @@ export class ProjectsService { ); } - addNameSearchFilter(searchNameText: string, params: ProjectParams): void { + addNameSearchFilter(searchNameText: string, params: PlatformProjectParams): void { if (typeof searchNameText !== 'undefined' && searchNameText !== null) { - params.project_name = 'ilike.%' + searchNameText + '%'; + params.name = 'ilike.%' + searchNameText + '%'; } } - addProjectIdsFilter(projectIds: number[], params: ProjectParams): void { + addProjectIdsFilter(projectIds: number[], params: PlatformProjectParams): void { if (typeof projectIds !== 'undefined' && projectIds !== null) { - params.project_id = 'in.(' + projectIds.join(',') + ')'; + params.id = 'in.(' + projectIds.join(',') + ')'; } } - addOrgCategoryIdsFilter(orgCategoryIds: string[], params: ProjectParams): void { + addOrgCategoryIdsFilter(orgCategoryIds: string[], params: PlatformProjectParams): void { if (typeof orgCategoryIds !== 'undefined' && orgCategoryIds !== null) { - params.project_org_category_ids = 'ov.{' + orgCategoryIds.join(',') + '}'; + params.or = '(category_ids.is.null, ' + 'category_ids.ov.{' + orgCategoryIds.join(',') + '}' + ')'; } } - addActiveFilter(active: boolean, params: ProjectParams): void { - if (typeof active !== 'undefined' && active !== null) { - params.project_active = 'eq.' + active; + addActiveFilter(isEnabled: boolean, params: PlatformProjectParams): void { + if (typeof isEnabled !== 'undefined' && isEnabled !== null) { + params.is_enabled = 'eq.' + isEnabled; } } @@ -125,41 +119,62 @@ export class ProjectsService { return categoryList; } - // TODO: We should remove this from being used and replace with transform - getAllActive(): Observable { + getAllActive(activeCategoryList?: OrgCategory[]): Observable { const data = { params: { - active_only: true, + is_enabled: `eq.true`, }, }; - return this.apiService.get('/projects', data).pipe( - map((res) => - res.map((datum) => ({ - ...datum, - created_at: new Date(datum.created_at), - updated_at: new Date(datum.updated_at), - })) - ) - ); + return this.spenderPlatformV1ApiService + .get>('/projects', data) + .pipe(map((res) => this.transformToV1Response(res.data, activeCategoryList))); } - getbyId(projectId: number | string): Observable { - return this.apiV2Service - .get('/projects', { + getbyId(projectId: number | string, activeCategoryList?: OrgCategory[]): Observable { + return this.spenderPlatformV1ApiService + .get>('/projects', { params: { - project_id: `eq.${projectId}`, + id: `eq.${projectId}`, }, }) - .pipe( - map( - (res) => - res.data.map((datum) => ({ - ...datum, - project_created_at: new Date(datum.project_created_at), - project_updated_at: new Date(datum.project_updated_at), - }))[0] - ) - ); + .pipe(map((res) => this.transformToV2Response(res.data, activeCategoryList)[0])); + } + + transformToV1Response(platformProject: PlatformProject[], activeCategoryList?: OrgCategory[]): ProjectV1[] { + const allCategoryIDs = activeCategoryList?.map((category) => category.id); + + const projectV1 = platformProject.map((platformProject) => ({ + id: platformProject.id, + created_at: new Date(platformProject.created_at), + updated_at: new Date(platformProject.updated_at), + name: platformProject.name, + sub_project: platformProject.sub_project, + code: platformProject.code, + org_id: platformProject.org_id, + description: platformProject.description, + active: platformProject.is_enabled, + org_category_ids: platformProject.category_ids === null ? allCategoryIDs : platformProject.category_ids, + })); + return projectV1; + } + + transformToV2Response(platformProject: PlatformProject[], activeCategoryList?: OrgCategory[]): ProjectV2[] { + const allCategoryIDs = activeCategoryList?.map((category) => category.id); + + const projectV2 = platformProject.map((platformProject) => ({ + project_active: platformProject.is_enabled, + project_code: platformProject.code, + project_created_at: new Date(platformProject.created_at), + project_description: platformProject.description, + project_id: platformProject.id, + project_name: platformProject.display_name, + project_org_category_ids: platformProject.category_ids === null ? allCategoryIDs : platformProject.category_ids, + project_org_id: platformProject.org_id, + project_updated_at: new Date(platformProject.updated_at), + projectv2_name: platformProject.name, + sub_project_name: platformProject.sub_project, + })); + return projectV2; } } diff --git a/src/app/core/services/recently-used-items.service.spec.ts b/src/app/core/services/recently-used-items.service.spec.ts index 437ab4cde6..683339c2a2 100644 --- a/src/app/core/services/recently-used-items.service.spec.ts +++ b/src/app/core/services/recently-used-items.service.spec.ts @@ -1,4 +1,4 @@ -import { TestBed } from '@angular/core/testing'; +import { TestBed, discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing'; import { ApiService } from './api.service'; import { ProjectsService } from './projects.service'; import { RecentlyUsedItemsService } from './recently-used-items.service'; @@ -16,11 +16,16 @@ import { import { apiEouRes } from '../mock-data/extended-org-user.data'; import { of } from 'rxjs'; import { recentUsedCategoriesRes } from '../mock-data/org-category-list-item.data'; +import { CategoriesService } from './categories.service'; +import { orgCategoryPaginated1 } from '../mock-data/org-category.data'; +import { testActiveCategoryList } from '../test-data/projects.spec.data'; +import { platformProjectsArgs1 } from '../mock-data/platform/v1/platform-project-args.data'; describe('RecentlyUsedItemsService', () => { let recentlyUsedItemsService: RecentlyUsedItemsService; let apiService: jasmine.SpyObj; let projectsService: jasmine.SpyObj; + let categoriesService: jasmine.SpyObj; beforeEach(() => { TestBed.configureTestingModule({ @@ -34,11 +39,16 @@ describe('RecentlyUsedItemsService', () => { provide: ProjectsService, useValue: jasmine.createSpyObj('ProjectsService', ['getByParamsUnformatted']), }, + { + provide: CategoriesService, + useValue: jasmine.createSpyObj('CategoriesService', ['getAll']), + }, ], }); recentlyUsedItemsService = TestBed.inject(RecentlyUsedItemsService); apiService = TestBed.inject(ApiService) as jasmine.SpyObj; projectsService = TestBed.inject(ProjectsService) as jasmine.SpyObj; + categoriesService = TestBed.inject(CategoriesService) as jasmine.SpyObj; }); it('should be created', () => { @@ -56,33 +66,34 @@ describe('RecentlyUsedItemsService', () => { describe('getRecentlyUsedProjects():', () => { it('should get all the recently used projects', (done) => { + categoriesService.getAll.and.returnValue(of(orgCategoryPaginated1)); projectsService.getByParamsUnformatted.and.returnValue(of(recentlyUsedProjectRes)); + const config = { recentValues: recentlyUsedRes, eou: apiEouRes, categoryIds: ['16558', '16559', '16560', '16561', '16562'], + activeCategoryList: testActiveCategoryList, }; + recentlyUsedItemsService.getRecentlyUsedProjects(config).subscribe((res) => { - expect(projectsService.getByParamsUnformatted).toHaveBeenCalledOnceWith({ - orgId: config.eou.ou.org_id, - active: true, - sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: config.categoryIds, - projectIds: config.recentValues.recent_project_ids, - offset: 0, - limit: 10, - }); + expect(projectsService.getByParamsUnformatted).toHaveBeenCalledOnceWith( + platformProjectsArgs1, + testActiveCategoryList + ); expect(res).toEqual(recentlyUsedProjectRes); done(); }); }); it('should return null when there are no recently used projects', (done) => { + categoriesService.getAll.and.returnValue(of(orgCategoryPaginated1)); + const config = { recentValues: null, eou: apiEouRes, categoryIds: ['16558', '16559', '16560', '16561', '16562'], + activeCategoryList: testActiveCategoryList, }; recentlyUsedItemsService.getRecentlyUsedProjects(config).subscribe((res) => { expect(res).toBeNull(); diff --git a/src/app/core/services/recently-used-items.service.ts b/src/app/core/services/recently-used-items.service.ts index 5a0bba0475..7e37cd6108 100644 --- a/src/app/core/services/recently-used-items.service.ts +++ b/src/app/core/services/recently-used-items.service.ts @@ -7,7 +7,7 @@ import { ProjectsService } from 'src/app/core/services/projects.service'; import { map } from 'rxjs/operators'; import { ProjectV2 } from '../models/v2/project-v2.model'; import { ExtendedOrgUser } from '../models/extended-org-user.model'; -import { OrgCategoryListItem } from '../models/v1/org-category.model'; +import { OrgCategory, OrgCategoryListItem } from '../models/v1/org-category.model'; import { Currency, CurrencyName } from '../models/currency.model'; @Injectable({ providedIn: 'root', @@ -23,6 +23,7 @@ export class RecentlyUsedItemsService { recentValues: RecentlyUsed; eou: ExtendedOrgUser; categoryIds: string[]; + activeCategoryList?: OrgCategory[]; }): Observable { if ( config.recentValues && @@ -31,16 +32,19 @@ export class RecentlyUsedItemsService { config.eou ) { return this.projectsService - .getByParamsUnformatted({ - orgId: config.eou.ou.org_id, - active: true, - sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: config.categoryIds, - projectIds: config.recentValues.recent_project_ids, - offset: 0, - limit: 10, - }) + .getByParamsUnformatted( + { + orgId: config.eou.ou.org_id, + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: config.categoryIds, + projectIds: config.recentValues.recent_project_ids, + offset: 0, + limit: 10, + }, + config.activeCategoryList + ) .pipe( map((project) => { const projectsMap: { [key: string]: ProjectV2 } = {}; diff --git a/src/app/core/test-data/projects.spec.data.ts b/src/app/core/test-data/projects.spec.data.ts index 64c42dcece..c191b487e2 100644 --- a/src/app/core/test-data/projects.spec.data.ts +++ b/src/app/core/test-data/projects.spec.data.ts @@ -1,6 +1,4 @@ import deepFreeze from 'deep-freeze-strict'; - -import { ProjectParams } from '../models/project-params.model'; import { ProjectV1 } from '../models/v1/extended-project.model'; import { OrgCategory, OrgCategoryListItem } from '../models/v1/org-category.model'; import { ProjectV2 } from '../models/v2/project-v2.model'; @@ -16,8 +14,6 @@ export const apiResponseActiveOnly = deepFreeze([ org_id: 'orFdTTTNcyye', description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', active: true, - approver1_id: null, - approver2_id: null, org_category_ids: [null, 145429, 122269, 122271], }, { @@ -30,8 +26,6 @@ export const apiResponseActiveOnly = deepFreeze([ org_id: 'orFdTTTNcyye', description: 'Sage Intacct Project - Sage Project 8, Id - 1178', active: true, - approver1_id: null, - approver2_id: null, org_category_ids: [null, 145429, 122269, 122271], }, { @@ -44,8 +38,6 @@ export const apiResponseActiveOnly = deepFreeze([ org_id: 'orFdTTTNcyye', description: 'Sage Intacct Project - Fyle Team Integrations, Id - 1183', active: true, - approver1_id: null, - approver2_id: null, org_category_ids: null, }, ]); @@ -61,8 +53,6 @@ export const expectedReponseActiveOnly = deepFreeze([ org_id: 'orFdTTTNcyye', description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', active: true, - approver1_id: null, - approver2_id: null, org_category_ids: [null, 145429, 122269, 122271], }, { @@ -75,8 +65,6 @@ export const expectedReponseActiveOnly = deepFreeze([ org_id: 'orFdTTTNcyye', description: 'Sage Intacct Project - Sage Project 8, Id - 1178', active: true, - approver1_id: null, - approver2_id: null, org_category_ids: [null, 145429, 122269, 122271], }, { @@ -89,8 +77,6 @@ export const expectedReponseActiveOnly = deepFreeze([ org_id: 'orFdTTTNcyye', description: 'Sage Intacct Project - Fyle Team Integrations, Id - 1183', active: true, - approver1_id: null, - approver2_id: null, org_category_ids: null, }, ]); @@ -99,13 +85,7 @@ export const apiV2ResponseMultiple = deepFreeze({ count: 2, data: [ { - ap1_email: null, - ap1_full_name: null, - ap2_email: null, - ap2_full_name: null, project_active: true, - project_approver1_id: null, - project_approver2_id: null, project_code: '1184', project_created_at: new Date('2021-05-12T10:28:40.834844'), project_description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', @@ -118,13 +98,7 @@ export const apiV2ResponseMultiple = deepFreeze({ sub_project_name: null, }, { - ap1_email: null, - ap1_full_name: null, - ap2_email: null, - ap2_full_name: null, project_active: true, - project_approver1_id: null, - project_approver2_id: null, project_code: '1182', project_created_at: new Date('2021-05-12T10:28:40.834844'), project_description: 'Sage Intacct Project - Fyle Engineering, Id - 1182', @@ -146,13 +120,7 @@ export const apiV2ResponseSingle = deepFreeze({ count: 1, data: [ { - ap1_email: null, - ap1_full_name: null, - ap2_email: null, - ap2_full_name: null, project_active: true, - project_approver1_id: null, - project_approver2_id: null, project_code: '1184', project_created_at: new Date('2021-05-12T10:28:40.834844'), project_description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', @@ -170,45 +138,6 @@ export const apiV2ResponseSingle = deepFreeze({ url: '/v2/projects', }); -export const testActiveCategoryList: OrgCategory[] = deepFreeze([ - { - code: '4060340', - created_at: new Date('2018-01-31T23:50:27.215171+00:00'), - displayName: 'Snacks', - enabled: true, - fyle_category: 'Food', - id: 16560, - name: 'Snacks', - org_id: 'orNVthTo2Zyo', - sub_category: 'Snacks', - updated_at: new Date('2022-11-23T14:25:26.485891+00:00'), - }, - { - code: '4060337', - created_at: new Date('2022-07-05T07:52:00.417939+00:00'), - displayName: 'Train / Induction', - enabled: true, - fyle_category: 'Train', - id: 201949, - name: 'Train', - org_id: 'orNVthTo2Zyo', - sub_category: 'Induction', - updated_at: new Date('2022-07-05T07:52:00.417939+00:00'), - }, - { - code: 'Cell phone', - created_at: new Date('2021-03-19T04:44:55.627307+00:00'), - displayName: 'Cell phone', - enabled: true, - fyle_category: null, - id: 130361, - name: 'Cell phone', - org_id: 'orNVthTo2Zyo', - sub_category: 'Cell phone', - updated_at: new Date('2022-05-05T17:46:15.434494+00:00'), - }, -]); - export const testActiveCategoryListOptions: OrgCategoryListItem[] = deepFreeze([ { label: 'Snacks', @@ -257,6 +186,45 @@ export const testActiveCategoryListOptions: OrgCategoryListItem[] = deepFreeze([ }, ]); +export const testActiveCategoryList: OrgCategory[] = deepFreeze([ + { + code: '4060340', + created_at: new Date('2018-01-31T23:50:27.215171+00:00'), + displayName: 'Snacks', + enabled: true, + fyle_category: 'Food', + id: 16560, + name: 'Snacks', + org_id: 'orNVthTo2Zyo', + sub_category: 'Snacks', + updated_at: new Date('2022-11-23T14:25:26.485891+00:00'), + }, + { + code: '4060337', + created_at: new Date('2022-07-05T07:52:00.417939+00:00'), + displayName: 'Train / Induction', + enabled: true, + fyle_category: 'Train', + id: 201949, + name: 'Train', + org_id: 'orNVthTo2Zyo', + sub_category: 'Induction', + updated_at: new Date('2022-07-05T07:52:00.417939+00:00'), + }, + { + code: 'Cell phone', + created_at: new Date('2021-03-19T04:44:55.627307+00:00'), + displayName: 'Cell phone', + enabled: true, + fyle_category: null, + id: 130361, + name: 'Cell phone', + org_id: 'orNVthTo2Zyo', + sub_category: 'Cell phone', + updated_at: new Date('2022-05-05T17:46:15.434494+00:00'), + }, +]); + export const allowedActiveCategories: OrgCategory[] = deepFreeze([ { code: '4060340', @@ -319,13 +287,7 @@ export const allowedActiveCategoriesListOptions: OrgCategoryListItem[] = deepFre export const expectedProjectsResponse: ProjectV2[] = deepFreeze([ { - ap1_email: null, - ap1_full_name: null, - ap2_email: null, - ap2_full_name: null, project_active: true, - project_approver1_id: null, - project_approver2_id: null, project_code: '1184', project_created_at: new Date('2021-05-12T10:28:40.834844'), project_description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', @@ -338,13 +300,7 @@ export const expectedProjectsResponse: ProjectV2[] = deepFreeze([ sub_project_name: null, }, { - ap1_email: null, - ap1_full_name: null, - ap2_email: null, - ap2_full_name: null, project_active: true, - project_approver1_id: null, - project_approver2_id: null, project_code: '1182', project_created_at: new Date('2021-05-12T10:28:40.834844'), project_description: 'Sage Intacct Project - Fyle Engineering, Id - 1182', @@ -358,12 +314,13 @@ export const expectedProjectsResponse: ProjectV2[] = deepFreeze([ }, ]); -export const testProjectParams: ProjectParams = deepFreeze({ +export const testProjectParams = deepFreeze({ orgId: 'orNVthTo2Zyo', - active: true, + isEnabled: true, sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: [null, '122269', '122270', '122271', '122272', '122273'], + sortOrder: 'name', + orgCategoryIds: ['122269', '122270', '122271', '122272', '122273'], + or: '(category_ids.is.null, category_ids.ov.{122269,122270,122271,122272,122273})', projectIds: [3943, 305792, 148971, 247936], offset: 0, limit: 10, @@ -390,6 +347,22 @@ export const testProjectV2: ProjectV2 = deepFreeze({ sub_project_name: null, }); +export const expectedV2WithAllCategories: ProjectV2[] = deepFreeze([ + { + project_active: true, + project_code: '1184', + project_created_at: new Date('2021-05-12T10:28:40.834Z'), + project_description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', + project_id: 257528, + project_name: 'Customer Mapped Project', + project_org_category_ids: testActiveCategoryList.map((category) => category.id), + project_org_id: 'orFdTTTNcyye', + project_updated_at: new Date('2021-07-08T10:28:27.686Z'), + projectv2_name: 'Customer Mapped Project', + sub_project_name: null, + }, +]); + export const testCategoryIds = deepFreeze(['145429', '140530', '145458', '122269']); export const params = deepFreeze({ @@ -405,23 +378,27 @@ export const params = deepFreeze({ export const projectsV1Data: ProjectV1[] = deepFreeze([ { + id: 257528, created_at: new Date('2021-05-12T10:28:40.834844'), updated_at: new Date('2021-07-08T10:28:27.686886'), - ...apiResponseActiveOnly[0], - }, - { - created_at: new Date('2021-05-12T10:28:40.834844'), - updated_at: new Date('2021-07-08T10:28:27.686886'), - ...apiResponseActiveOnly[1], + name: 'Customer Mapped Project', + sub_project: null, + code: '1184', + org_id: 'orFdTTTNcyye', + description: 'Sage Intacct Project - Customer Mapped Project, Id - 1184', + active: true, + org_category_ids: [122269, 122270, 122271, null], }, -]); - -export const projectsV1Data2: ProjectV1[] = deepFreeze([ { + id: 257529, created_at: new Date('2021-05-12T10:28:40.834844'), updated_at: new Date('2021-07-08T10:28:27.686886'), - id: 3943, - name: 'Staging Project', - ...apiResponseActiveOnly[0], + name: 'Fyle Engineering', + sub_project: null, + code: '1182', + org_id: 'orFdTTTNcyye', + active: true, + description: 'Sage Intacct Project - Fyle Engineering, Id - 1182', + org_category_ids: [122269, 122270, 122271], }, ]); diff --git a/src/app/fyle/add-edit-expense/add-edit-expense-1.spec.ts b/src/app/fyle/add-edit-expense/add-edit-expense-1.spec.ts index 615c94d446..83da15053e 100644 --- a/src/app/fyle/add-edit-expense/add-edit-expense-1.spec.ts +++ b/src/app/fyle/add-edit-expense/add-edit-expense-1.spec.ts @@ -66,6 +66,7 @@ import { expenseResponseData } from 'src/app/core/mock-data/platform/v1/expense. import { expenseFieldResponse } from 'src/app/core/mock-data/expense-field.data'; import { expectedProjects4 } from 'src/app/core/mock-data/extended-projects.data'; import { reportData1 } from 'src/app/core/mock-data/report.data'; +import { sortedCategory } from 'src/app/core/mock-data/org-category.data'; export function TestCases1(getTestBed) { return describe('AddEditExpensePage-1', () => { @@ -833,8 +834,8 @@ export function TestCases1(getTestBed) { expect(popoverController.create).toHaveBeenCalledOnceWith( component.getRemoveCCCExpModalParams(header, body, ctaText, ctaLoadingText) ); - expect(trackingService.unlinkCorporateCardExpense).toHaveBeenCalledTimes(1), - expect(component.goBack).toHaveBeenCalledOnceWith(); + expect(trackingService.unlinkCorporateCardExpense).toHaveBeenCalledTimes(1); + expect(component.goBack).toHaveBeenCalledOnceWith(); expect(component.showSnackBarToast).toHaveBeenCalledOnceWith( { message: 'Successfully removed the card details from the expense.' }, 'information', @@ -1191,6 +1192,7 @@ export function TestCases1(getTestBed) { expense_settings: { ...orgSettingsData.expense_settings, split_expense_settings: { enabled: true } }, }) ); + component.activeCategories$ = of(sortedCategory); component.costCenters$ = of(expectedCCdata); projectsService.getAllActive.and.returnValue(of(projectsV1Data)); component.filteredCategories$ = of(categorieListRes); @@ -1243,6 +1245,7 @@ export function TestCases1(getTestBed) { expense_settings: { ...orgSettingsData.expense_settings, split_expense_settings: { enabled: true } }, }) ); + component.activeCategories$ = of(sortedCategory); component.costCenters$ = of(expectedCCdata); projectsService.getAllActive.and.returnValue(of(projectsV1Data)); component.filteredCategories$ = of(categorieListRes); @@ -1296,6 +1299,7 @@ export function TestCases1(getTestBed) { expense_settings: { ...orgSettingsData.expense_settings, split_expense_settings: { enabled: false } }, }) ); + component.activeCategories$ = of(sortedCategory); component.costCenters$ = of(expectedCCdata); projectsService.getAllActive.and.returnValue(of(projectsV1Data)); component.filteredCategories$ = of(categorieListRes); @@ -1329,6 +1333,7 @@ export function TestCases1(getTestBed) { expense_settings: { ...orgSettingsData.expense_settings, split_expense_settings: { enabled: false } }, }) ); + component.activeCategories$ = of(sortedCategory); component.costCenters$ = of(expectedCCdata); projectsService.getAllActive.and.returnValue(of(projectsV1Data)); component.filteredCategories$ = of(categorieListRes); @@ -1360,6 +1365,7 @@ export function TestCases1(getTestBed) { expense_settings: { ...orgSettingsData.expense_settings, split_expense_settings: { enabled: false } }, }) ); + component.activeCategories$ = of(sortedCategory); component.costCenters$ = of(expectedCCdata); projectsService.getAllActive.and.returnValue(of(projectsV1Data)); component.filteredCategories$ = of(categorieListRes); diff --git a/src/app/fyle/add-edit-expense/add-edit-expense-5.spec.ts b/src/app/fyle/add-edit-expense/add-edit-expense-5.spec.ts index eebf429c10..21c2369359 100644 --- a/src/app/fyle/add-edit-expense/add-edit-expense-5.spec.ts +++ b/src/app/fyle/add-edit-expense/add-edit-expense-5.spec.ts @@ -101,7 +101,11 @@ import { } from 'src/app/core/test-data/accounts.service.spec.data'; import { customInputData, filledCustomProperties } from 'src/app/core/test-data/custom-inputs.spec.data'; import { txnCustomProperties, txnCustomProperties2 } from 'src/app/core/test-data/dependent-fields.service.spec.data'; -import { apiV2ResponseMultiple, expectedProjectsResponse } from 'src/app/core/test-data/projects.spec.data'; +import { + apiV2ResponseMultiple, + expectedProjectsResponse, + testActiveCategoryList, +} from 'src/app/core/test-data/projects.spec.data'; import { getEstatusApiResponse } from 'src/app/core/test-data/status.service.spec.data'; import { AddEditExpensePage } from './add-edit-expense.page'; import { txnFieldsData2, txnFieldsFlightData } from 'src/app/core/mock-data/expense-fields-map.data'; @@ -409,9 +413,11 @@ export function TestCases5(getTestBed) { describe('setupFilteredCategories():', () => { it('should get filtered categories for a project', fakeAsync(() => { component.etxn$ = of(unflattenedTxnData); + component.activeCategories$ = of(sortedCategory); + projectsService.getbyId.and.returnValue(of(apiV2ResponseMultiple[0])); projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories); - component.setupFilteredCategories(of(sortedCategory)); + component.setupFilteredCategories(); tick(500); component.fg.controls.project.setValue(apiV2ResponseMultiple[1]); @@ -419,33 +425,35 @@ export function TestCases5(getTestBed) { tick(500); expect(component.fg.controls.billable.value).toBeFalse(); - expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id, sortedCategory); expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(apiV2ResponseMultiple[1], sortedCategory); })); it('should get updated filtered categories for changing an existing project', fakeAsync(() => { component.etxn$ = of(unflattenedExpWoProject); + component.activeCategories$ = of(sortedCategory); component.fg.controls.project.setValue(expectedProjectsResponse[0]); component.fg.controls.category.setValue(orgCategoryData); projectsService.getbyId.and.returnValue(of(apiV2ResponseMultiple[0])); projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories); - component.setupFilteredCategories(of(sortedCategory)); + component.setupFilteredCategories(); tick(500); component.fg.controls.project.setValue(apiV2ResponseMultiple[1]); fixture.detectChanges(); tick(500); - expect(projectsService.getbyId).toHaveBeenCalledOnceWith(257528); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith(257528, sortedCategory); expect(component.fg.controls.billable.value).toBeFalse(); expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledWith(apiV2ResponseMultiple[1], sortedCategory); })); it('should return null the expense does not have project id', fakeAsync(() => { component.etxn$ = of(unflattenedExpWoProject); + component.activeCategories$ = of(sortedCategory); component.fg.controls.project.reset(); projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories); - component.setupFilteredCategories(of(sortedCategory)); + component.setupFilteredCategories(); tick(500); component.fg.controls.project.setValue(null); @@ -548,18 +556,20 @@ export function TestCases5(getTestBed) { describe('getSelectedProjects():', () => { it('should return the selected project from the expense', (done) => { component.etxn$ = of(unflattenedTxnData); + component.activeCategories$ = of(sortedCategory); projectsService.getbyId.and.returnValue(of(expectedProjectsResponse[0])); fixture.detectChanges(); component.getSelectedProjects().subscribe((res) => { expect(res).toEqual(expectedProjectsResponse[0]); - expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id, sortedCategory); done(); }); }); it('should return project from the default ID specified in org', (done) => { component.etxn$ = of(unflattenedExpWoProject); + component.activeCategories$ = of(sortedCategory); orgSettingsService.get.and.returnValue(of(orgSettingsData)); component.orgUserSettings$ = of(orgUserSettingsData); projectsService.getbyId.and.returnValue(of(expectedProjectsResponse[0])); @@ -568,7 +578,10 @@ export function TestCases5(getTestBed) { component.getSelectedProjects().subscribe((res) => { expect(res).toEqual(expectedProjectsResponse[0]); expect(orgSettingsService.get).toHaveBeenCalledTimes(1); - expect(projectsService.getbyId).toHaveBeenCalledOnceWith(orgUserSettingsData.preferences.default_project_id); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith( + orgUserSettingsData.preferences.default_project_id, + sortedCategory + ); done(); }); }); @@ -684,6 +697,7 @@ export function TestCases5(getTestBed) { }); it('getRecentProjects(): should get recent projects', (done) => { + component.activeCategories$ = of(sortedCategory); component.recentlyUsedValues$ = of(recentlyUsedRes); authService.getEou.and.resolveTo(apiEouRes); component.fg.controls.category.setValue(orgCategoryData); @@ -697,6 +711,7 @@ export function TestCases5(getTestBed) { recentValues: recentlyUsedRes, eou: apiEouRes, categoryIds: component.fg.controls.category.value && component.fg.controls.category.value.id, + activeCategoryList: sortedCategory, }); done(); }); @@ -1505,7 +1520,7 @@ export function TestCases5(getTestBed) { expect(res).toBeUndefined(); }); - expect(component.setupFilteredCategories).toHaveBeenCalledOnceWith(jasmine.any(Observable)); + expect(component.setupFilteredCategories).toHaveBeenCalledTimes(1); expect(component.setupExpenseFields).toHaveBeenCalledTimes(1); component.taxSettings$.subscribe((res) => { @@ -1790,7 +1805,7 @@ export function TestCases5(getTestBed) { expect(res).toBeUndefined(); }); - expect(component.setupFilteredCategories).toHaveBeenCalledOnceWith(jasmine.any(Observable)); + expect(component.setupFilteredCategories).toHaveBeenCalledTimes(1); expect(component.setupExpenseFields).toHaveBeenCalledTimes(1); component.taxSettings$.subscribe((res) => { diff --git a/src/app/fyle/add-edit-expense/add-edit-expense.page.ts b/src/app/fyle/add-edit-expense/add-edit-expense.page.ts index 2160eddc2e..1a13ad80f9 100644 --- a/src/app/fyle/add-edit-expense/add-edit-expense.page.ts +++ b/src/app/fyle/add-edit-expense/add-edit-expense.page.ts @@ -7,7 +7,6 @@ import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar'; import { DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { ActionSheetController, ModalController, NavController, Platform, PopoverController } from '@ionic/angular'; - import * as dayjs from 'dayjs'; import { cloneDeep, isEqual, isNull, isNumber, mergeWith } from 'lodash'; import { @@ -428,6 +427,8 @@ export class AddEditExpensePage implements OnInit { pendingTransactionAllowedToReportAndSplit = true; + activeCategories$: Observable; + constructor( private activatedRoute: ActivatedRoute, private accountsService: AccountsService, @@ -994,10 +995,13 @@ export class AddEditExpensePage implements OnInit { } getActionSheetOptions(): Observable<{ text: string; handler: () => void }[]> { + const projects$ = this.activeCategories$.pipe( + switchMap((activeCategories) => this.projectsService.getAllActive(activeCategories)) + ); return forkJoin({ orgSettings: this.orgSettingsService.get(), costCenters: this.costCenters$, - projects: this.projectsService.getAllActive(), + projects: projects$, txnFields: this.txnFields$.pipe(take(1)), filteredCategories: this.filteredCategories$.pipe(take(1)), showProjectMappedCategoriesInSplitExpense: this.launchDarklyService.getVariation( @@ -1465,7 +1469,9 @@ export class AddEditExpensePage implements OnInit { }), switchMap((projectId) => { if (projectId) { - return this.projectsService.getbyId(projectId); + return this.activeCategories$.pipe( + switchMap((allActiveCategories) => this.projectsService.getbyId(projectId, allActiveCategories)) + ); } else { return of(null); } @@ -1552,14 +1558,16 @@ export class AddEditExpensePage implements OnInit { return forkJoin({ recentValues: this.recentlyUsedValues$, eou: this.authService.getEou(), + activeCategories: this.activeCategories$, }).pipe( - switchMap(({ recentValues, eou }) => { + switchMap(({ recentValues, eou, activeCategories }) => { const formControl = this.getFormControl('category') as { value: OrgCategory }; const categoryId = formControl.value && (formControl.value.id as unknown as string[]); return this.recentlyUsedItemsService.getRecentlyUsedProjects({ recentValues, eou, categoryIds: categoryId, + activeCategoryList: activeCategories, }); }) ); @@ -2523,7 +2531,7 @@ export class AddEditExpensePage implements OnInit { this.updateFormForExpenseFields(txnFieldsMap$); } - setupFilteredCategories(activeCategories$: Observable): void { + setupFilteredCategories(): void { const projectControl = this.fg.controls.project as { value: { project_id: number; @@ -2533,9 +2541,15 @@ export class AddEditExpensePage implements OnInit { this.filteredCategories$ = this.etxn$.pipe( switchMap((etxn) => { if (etxn.tx.project_id) { - return this.projectsService.getbyId(etxn.tx.project_id); + return this.activeCategories$.pipe( + map((allActiveCategories) => this.projectsService.getbyId(etxn.tx.project_id, allActiveCategories)) + ); } else if (projectControl?.value?.project_id) { - return this.projectsService.getbyId(projectControl.value.project_id); + return this.activeCategories$.pipe( + map((allActiveCategories) => + this.projectsService.getbyId(projectControl.value.project_id, allActiveCategories) + ) + ); } else { return of(null); } @@ -2551,7 +2565,7 @@ export class AddEditExpensePage implements OnInit { }), startWith(initialProject), concatMap((project: ProjectV2) => - activeCategories$.pipe( + this.activeCategories$.pipe( map((activeCategories) => this.projectsService.getAllowedOrgCategoryIds(project, activeCategories)) ) ), @@ -2862,6 +2876,8 @@ export class AddEditExpensePage implements OnInit { } ionViewWillEnter(): void { + this.activeCategories$ = this.getActiveCategories().pipe(shareReplay(1)); + this.initClassObservables(); this.newExpenseDataUrls = []; @@ -2988,10 +3004,14 @@ export class AddEditExpensePage implements OnInit { map((orgSettings) => orgSettings.advanced_projects && orgSettings.advanced_projects.enable_individual_projects) ); + const projectCount$ = this.activeCategories$.pipe( + switchMap((allActiveCategories) => this.projectsService.getProjectCount({ categoryIds: [] }, allActiveCategories)) + ); + this.isProjectsVisible$ = forkJoin({ individualProjectIds: this.individualProjectIds$, isIndividualProjectsEnabled: this.isIndividualProjectsEnabled$, - projectsCount: this.projectsService.getProjectCount(), + projectsCount: projectCount$, }).pipe( map(({ individualProjectIds, isIndividualProjectsEnabled, projectsCount }) => { if (!isIndividualProjectsEnabled) { @@ -3036,8 +3056,6 @@ export class AddEditExpensePage implements OnInit { this.minDate = dayjs(new Date('Jan 1, 2001')).format('YYYY-MM-D'); this.maxDate = dayjs(this.dateService.addDaysToDate(today, 1)).format('YYYY-MM-D'); - const activeCategories$ = this.getActiveCategories(); - this.paymentAccount$ = accounts$.pipe( map((accounts) => { if (!this.activatedRoute.snapshot.params.id && this.activatedRoute.snapshot.params.bankTxn) { @@ -3133,7 +3151,7 @@ export class AddEditExpensePage implements OnInit { this.initSplitTxn(orgSettings$); - this.setupFilteredCategories(activeCategories$); + this.setupFilteredCategories(); this.setupExpenseFields(); diff --git a/src/app/fyle/add-edit-mileage/add-edit-mileage-3.spec.ts b/src/app/fyle/add-edit-mileage/add-edit-mileage-3.spec.ts index af4d2bbf63..45d0328f2b 100644 --- a/src/app/fyle/add-edit-mileage/add-edit-mileage-3.spec.ts +++ b/src/app/fyle/add-edit-mileage/add-edit-mileage-3.spec.ts @@ -485,12 +485,13 @@ export function TestCases3(getTestBed) { describe('setupFilteredCategories():', () => { it('should set up filtered categories', fakeAsync(() => { + component.subCategories$ = of(sortedCategory); projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories); spyOn(component, 'getFormValues').and.returnValue({ sub_category: orgCategoryData, }); - component.setupFilteredCategories(of(sortedCategory)); + component.setupFilteredCategories(); tick(500); component.fg.controls.project.setValue(expectedProjectsResponse[0]); @@ -507,12 +508,13 @@ export function TestCases3(getTestBed) { })); it('should set up filtered categories and set default billable value if project is removed', fakeAsync(() => { + component.subCategories$ = of(sortedCategory); projectsService.getAllowedOrgCategoryIds.and.returnValue(transformedOrgCategories); spyOn(component, 'getFormValues').and.returnValue({ sub_category: orgCategoryData, }); - component.setupFilteredCategories(of(sortedCategory)); + component.setupFilteredCategories(); tick(500); component.fg.controls.project.setValue(null); diff --git a/src/app/fyle/add-edit-mileage/add-edit-mileage-4.spec.ts b/src/app/fyle/add-edit-mileage/add-edit-mileage-4.spec.ts index aa789297f4..a8a3d40f4c 100644 --- a/src/app/fyle/add-edit-mileage/add-edit-mileage-4.spec.ts +++ b/src/app/fyle/add-edit-mileage/add-edit-mileage-4.spec.ts @@ -23,6 +23,7 @@ import { mileageCategories3, orgCategoryData, unsortedCategories1, + sortedCategory, } from 'src/app/core/mock-data/org-category.data'; import { orgSettingsCCDisabled, @@ -494,18 +495,20 @@ export function TestCases4(getTestBed) { describe('getProjects():', () => { it('should return project from ID specified in the expense', (done) => { component.etxn$ = of(unflattenedTxnData); + component.subCategories$ = of(sortedCategory); projectsService.getbyId.and.returnValue(of(expectedProjectsResponse[0])); fixture.detectChanges(); component.getProjects().subscribe((res) => { expect(res).toEqual(expectedProjectsResponse[0]); - expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith(unflattenedTxnData.tx.project_id, sortedCategory); done(); }); }); it('should get default project ID and return the project if not provided in the expense', (done) => { component.etxn$ = of(newUnflattenedTxn); + component.subCategories$ = of(sortedCategory); orgSettingsService.get.and.returnValue(of(orgSettingsRes)); orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); projectsService.getbyId.and.returnValue(of(expectedProjectsResponse[0])); @@ -515,7 +518,10 @@ export function TestCases4(getTestBed) { expect(res).toEqual(expectedProjectsResponse[0]); expect(orgSettingsService.get).toHaveBeenCalledTimes(1); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(1); - expect(projectsService.getbyId).toHaveBeenCalledOnceWith(orgUserSettingsData.preferences.default_project_id); + expect(projectsService.getbyId).toHaveBeenCalledOnceWith( + orgUserSettingsData.preferences.default_project_id, + sortedCategory + ); done(); }); }); diff --git a/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts b/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts index dc738858a7..1b6b1b1317 100644 --- a/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts +++ b/src/app/fyle/add-edit-mileage/add-edit-mileage.page.ts @@ -454,7 +454,7 @@ export class AddEditMileagePage implements OnInit { this.connectionStatus$ = this.isConnected$.pipe(map((isConnected) => ({ connected: isConnected }))); } - setupFilteredCategories(activeCategories$: Observable): void { + setupFilteredCategories(): void { this.filteredCategories$ = this.fg.controls.project.valueChanges.pipe( tap(() => { if (!this.fg.controls.project.value) { @@ -465,9 +465,9 @@ export class AddEditMileagePage implements OnInit { }), startWith(this.fg.controls.project.value), concatMap((project: ProjectV2) => - activeCategories$.pipe( - map((activeCategories: OrgCategory[]) => - this.projectsService.getAllowedOrgCategoryIds(project, activeCategories) + this.subCategories$.pipe( + map((allActiveSubCategories: OrgCategory[]) => + this.projectsService.getAllowedOrgCategoryIds(project, allActiveSubCategories) ) ) ), @@ -1160,7 +1160,9 @@ export class AddEditMileagePage implements OnInit { }), switchMap((projectId) => { if (projectId) { - return this.projectsService.getbyId(projectId); + return this.subCategories$.pipe( + switchMap((allActiveSubCategories) => this.projectsService.getbyId(projectId, allActiveSubCategories)) + ); } else { return of(null); } @@ -1493,6 +1495,7 @@ export class AddEditMileagePage implements OnInit { } ionViewWillEnter(): void { + this.subCategories$ = this.getSubCategories().pipe(shareReplay(1)); this.initClassObservables(); from(this.tokenService.getClusterDomain()).subscribe((clusterDomain) => { @@ -1566,11 +1569,13 @@ export class AddEditMileagePage implements OnInit { this.txnFields$ = this.getTransactionFields(); this.homeCurrency$ = this.currencyService.getHomeCurrency(); - this.subCategories$ = this.getSubCategories(); - this.setupFilteredCategories(this.subCategories$); + + this.setupFilteredCategories(); this.projectCategoryIds$ = this.getProjectCategoryIds(); - this.isProjectVisible$ = this.projectCategoryIds$.pipe( - switchMap((projectCategoryIds) => this.projectsService.getProjectCount({ categoryIds: projectCategoryIds })), + this.isProjectVisible$ = combineLatest([this.projectCategoryIds$, this.subCategories$]).pipe( + switchMap(([projectCategoryIds, allActiveSubCategories]) => + this.projectsService.getProjectCount({ categoryIds: projectCategoryIds }, allActiveSubCategories) + ), map((projectCount) => projectCount > 0) ); this.comments$ = this.statusService.find('transactions', this.activatedRoute.snapshot.params.id as string); @@ -1700,12 +1705,14 @@ export class AddEditMileagePage implements OnInit { recentValues: this.recentlyUsedValues$, mileageCategoryIds: this.projectCategoryIds$, eou: eou$, + activeSubCategories: this.subCategories$, }).pipe( - switchMap(({ recentValues, mileageCategoryIds, eou }) => + switchMap(({ recentValues, mileageCategoryIds, eou, activeSubCategories }) => this.recentlyUsedItemsService.getRecentlyUsedProjects({ recentValues, eou, categoryIds: mileageCategoryIds, + activeCategoryList: activeSubCategories, }) ) ); diff --git a/src/app/fyle/add-edit-per-diem/add-edit-per-diem-2.page.spec.ts b/src/app/fyle/add-edit-per-diem/add-edit-per-diem-2.page.spec.ts index 449596a332..bd961c5266 100644 --- a/src/app/fyle/add-edit-per-diem/add-edit-per-diem-2.page.spec.ts +++ b/src/app/fyle/add-edit-per-diem/add-edit-per-diem-2.page.spec.ts @@ -230,6 +230,7 @@ export function TestCases2(getTestBed) { }); it('setupFilteredCategories(): should setup filteredCategories$', () => { + component.subCategories$ = of(orgCategoryData1); component.fg.patchValue({ sub_category: { id: 247980, @@ -238,7 +239,7 @@ export function TestCases2(getTestBed) { }); projectsService.getAllowedOrgCategoryIds.and.returnValue([orgCategoryData]); spyOn(component.fg.controls.sub_category, 'reset'); - component.setupFilteredCategories(of(orgCategoryData1)); + component.setupFilteredCategories(); expect(projectsService.getAllowedOrgCategoryIds).toHaveBeenCalledOnceWith(projects[0], orgCategoryData1); expect(component.fg.controls.sub_category.reset).toHaveBeenCalledTimes(1); }); @@ -620,9 +621,12 @@ export function TestCases2(getTestBed) { }); component.isProjectVisible$.subscribe((res) => { - expect(projectsService.getProjectCount).toHaveBeenCalledOnceWith({ - categoryIds: ['129140', '129112', '16582', '201952'], - }); + expect(projectsService.getProjectCount).toHaveBeenCalledOnceWith( + { + categoryIds: ['129140', '129112', '16582', '201952'], + }, + orgCategoryData1 + ); expect(res).toBeTrue(); }); diff --git a/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts b/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts index cfe1cb815b..a42a5559c3 100644 --- a/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts +++ b/src/app/fyle/add-edit-per-diem/add-edit-per-diem.page.ts @@ -677,7 +677,7 @@ export class AddEditPerDiemPage implements OnInit { ); } - setupFilteredCategories(activeCategories$: Observable): void { + setupFilteredCategories(): void { this.filteredCategories$ = this.fg.controls.project.valueChanges.pipe( tap(() => { if (!this.fg.controls.project.value) { @@ -688,8 +688,10 @@ export class AddEditPerDiemPage implements OnInit { }), startWith(this.fg.controls.project.value), concatMap((project: ProjectV2) => - activeCategories$.pipe( - map((activeCategories) => this.projectsService.getAllowedOrgCategoryIds(project, activeCategories)) + this.subCategories$.pipe( + map((allActiveSubCategories: OrgCategory[]) => + this.projectsService.getAllowedOrgCategoryIds(project, allActiveSubCategories) + ) ) ), map((categories) => categories.map((category) => ({ label: category.sub_category, value: category }))) @@ -842,6 +844,7 @@ export class AddEditPerDiemPage implements OnInit { } ionViewWillEnter(): void { + this.subCategories$ = this.getSubCategories().pipe(shareReplay(1)); this.isNewReportsFlowEnabled = false; this.onPageExit$ = new Subject(); this.projectDependentFieldsRef?.ngOnInit(); @@ -1003,12 +1006,14 @@ export class AddEditPerDiemPage implements OnInit { this.txnFields$ = this.getTransactionFields(); this.homeCurrency$ = this.currencyService.getHomeCurrency(); - this.subCategories$ = this.getSubCategories(); - this.setupFilteredCategories(this.subCategories$); + + this.setupFilteredCategories(); this.projectCategoryIds$ = this.getProjectCategoryIds(); - this.isProjectVisible$ = this.projectCategoryIds$.pipe( - switchMap((projectCategoryIds) => this.projectsService.getProjectCount({ categoryIds: projectCategoryIds })), + this.isProjectVisible$ = combineLatest([this.projectCategoryIds$, this.subCategories$]).pipe( + switchMap(([projectCategoryIds, allActiveSubCategories]) => + this.projectsService.getProjectCount({ categoryIds: projectCategoryIds }, allActiveSubCategories) + ), map((projectCount) => projectCount > 0) ); this.comments$ = this.statusService.find('transactions', this.activatedRoute.snapshot.params.id as string); @@ -1309,7 +1314,9 @@ export class AddEditPerDiemPage implements OnInit { }), switchMap((projectId) => { if (projectId) { - return this.projectsService.getbyId(projectId); + return this.subCategories$.pipe( + switchMap((allActiveSubCategories) => this.projectsService.getbyId(projectId, allActiveSubCategories)) + ); } else { return of(null); } @@ -1336,12 +1343,14 @@ export class AddEditPerDiemPage implements OnInit { recentValues: this.recentlyUsedValues$, perDiemCategoryIds: this.projectCategoryIds$, eou: this.authService.getEou(), + activeSubCategories: this.subCategories$, }).pipe( - switchMap(({ recentValues, perDiemCategoryIds, eou }) => + switchMap(({ recentValues, perDiemCategoryIds, eou, activeSubCategories }) => this.recentlyUsedItemsService.getRecentlyUsedProjects({ recentValues, eou, categoryIds: perDiemCategoryIds, + activeCategoryList: activeSubCategories, }) ) ); @@ -1351,7 +1360,9 @@ export class AddEditPerDiemPage implements OnInit { iif( () => !!etxn.tx.org_category_id, this.subCategories$.pipe( - map((subCategories) => subCategories.find((subCategory) => subCategory.id === etxn.tx.org_category_id)) + map((allActiveSubCategories) => + allActiveSubCategories.find((subCategory) => subCategory.id === etxn.tx.org_category_id) + ) ), of(null) ) diff --git a/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.spec.ts b/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.spec.ts index 13005d416b..e0182388d3 100644 --- a/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.spec.ts +++ b/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.spec.ts @@ -34,6 +34,14 @@ import { } from 'src/app/core/mock-data/extended-projects.data'; import { click, getAllElementsBySelector, getElementBySelector, getTextContent } from 'src/app/core/dom-helpers'; import { By } from '@angular/platform-browser'; +import { CategoriesService } from 'src/app/core/services/categories.service'; +import { + categoryIds, + expectedAllOrgCategories, + orgCategoryData, + orgCategoryPaginated1, + sortedCategory, +} from 'src/app/core/mock-data/org-category.data'; describe('FyProjectSelectModalComponent', () => { let component: FyProjectSelectModalComponent; @@ -41,6 +49,7 @@ describe('FyProjectSelectModalComponent', () => { let modalController: jasmine.SpyObj; let cdr: ChangeDetectorRef; let projectsService: jasmine.SpyObj; + let categoriesService: jasmine.SpyObj; let authService: jasmine.SpyObj; let recentLocalStorageItemsService: jasmine.SpyObj; let utilityService: jasmine.SpyObj; @@ -56,6 +65,11 @@ describe('FyProjectSelectModalComponent', () => { const utilityServiceSpy = jasmine.createSpyObj('UtilityService', ['searchArrayStream']); const orgUserSettingsServiceSpy = jasmine.createSpyObj('OrgUserSettingsService', ['get']); const orgSettingsServiceSpy = jasmine.createSpyObj('OrgSettingsService', ['get']); + const categoriesServiceSpy = jasmine.createSpyObj('CategoriesService', [ + 'getAll', + 'filterRequired', + 'getCategoryById', + ]); TestBed.configureTestingModule({ declarations: [FyProjectSelectModalComponent, FyHighlightTextComponent, HighlightPipe], @@ -98,6 +112,10 @@ describe('FyProjectSelectModalComponent', () => { provide: OrgUserSettingsService, useValue: orgUserSettingsServiceSpy, }, + { + provide: CategoriesService, + useValue: categoriesServiceSpy, + }, ], }).compileComponents(); fixture = TestBed.createComponent(FyProjectSelectModalComponent); @@ -106,6 +124,7 @@ describe('FyProjectSelectModalComponent', () => { modalController = TestBed.inject(ModalController) as jasmine.SpyObj; cdr = TestBed.inject(ChangeDetectorRef); projectsService = TestBed.inject(ProjectsService) as jasmine.SpyObj; + categoriesService = TestBed.inject(CategoriesService) as jasmine.SpyObj; authService = TestBed.inject(AuthService) as jasmine.SpyObj; recentLocalStorageItemsService = TestBed.inject( RecentLocalStorageItemsService @@ -120,6 +139,9 @@ describe('FyProjectSelectModalComponent', () => { authService.getEou.and.resolveTo(apiEouRes); orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); + categoriesService.getAll.and.returnValue(of([orgCategoryData])); + categoriesService.getCategoryById.and.returnValue(of(orgCategoryPaginated1[0])); + projectsService.getByParamsUnformatted.and.returnValue(of([singleProject2])); component.cacheName = 'projects'; @@ -147,18 +169,21 @@ describe('FyProjectSelectModalComponent', () => { expect(orgSettingsService.get).toHaveBeenCalledTimes(2); expect(authService.getEou).toHaveBeenCalledTimes(2); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(4); - expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith({ - orgId: 'orNVthTo2Zyo', - active: true, - sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: undefined, - projectIds: null, - searchNameText: '', - offset: 0, - limit: 20, - }); - expect(projectsService.getbyId).toHaveBeenCalledWith(3943); + expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith( + { + orgId: 'orNVthTo2Zyo', + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: undefined, + projectIds: null, + searchNameText: '', + offset: 0, + limit: 20, + }, + undefined + ); + expect(projectsService.getbyId).toHaveBeenCalledWith(3943, undefined); done(); }); }); @@ -173,23 +198,27 @@ describe('FyProjectSelectModalComponent', () => { expect(orgSettingsService.get).toHaveBeenCalledTimes(2); expect(authService.getEou).toHaveBeenCalledTimes(2); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(4); - expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith({ - orgId: 'orNVthTo2Zyo', - active: true, - sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: undefined, - projectIds: null, - searchNameText: '', - offset: 0, - limit: 20, - }); - expect(projectsService.getbyId).toHaveBeenCalledWith(3943); + expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith( + { + orgId: 'orNVthTo2Zyo', + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: undefined, + projectIds: null, + searchNameText: '', + offset: 0, + limit: 20, + }, + undefined + ); + expect(projectsService.getbyId).toHaveBeenCalledWith(3943, undefined); done(); }); }); it('should get projects when default value is null and no default projects are available', (done) => { + component.activeCategories$ = of(sortedCategory); orgSettingsService.get.and.returnValue(of(orgSettingsDataWithoutAdvPro)); projectsService.getbyId.and.returnValue(of(expectedProjects[0].value)); component.defaultValue = false; @@ -199,21 +228,59 @@ describe('FyProjectSelectModalComponent', () => { expect(orgSettingsService.get).toHaveBeenCalledTimes(2); expect(authService.getEou).toHaveBeenCalledTimes(2); expect(orgUserSettingsService.get).toHaveBeenCalledTimes(4); - expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith({ - orgId: 'orNVthTo2Zyo', - active: true, - sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: undefined, - projectIds: null, - searchNameText: '', - offset: 0, - limit: 20, - }); - expect(projectsService.getbyId).toHaveBeenCalledWith(3943); + expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith( + { + orgId: 'orNVthTo2Zyo', + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: undefined, + projectIds: null, + searchNameText: '', + offset: 0, + limit: 20, + }, + undefined + ); + expect(projectsService.getbyId).toHaveBeenCalledWith(3943, undefined); done(); }); }); + + it('should return an empty list when no projects match the search criteria', (done) => { + projectsService.getByParamsUnformatted.and.returnValue(of([])); + projectsService.getbyId.and.returnValue(of(null)); + authService.getEou.and.resolveTo(apiEouRes); + orgSettingsService.get.and.returnValue(of(orgSettingsData)); + orgUserSettingsService.get.and.returnValue(of(orgUserSettingsData)); + component.currentSelection = null; + component.defaultValue = false; + component.activeCategories$ = of([]); + + component.getProjects('nonexistent').subscribe((res) => { + expect(res).toEqual([{ label: 'None', value: null }]); + expect(orgSettingsService.get).toHaveBeenCalledTimes(2); + expect(authService.getEou).toHaveBeenCalledTimes(2); + expect(orgUserSettingsService.get).toHaveBeenCalledTimes(4); + expect(projectsService.getByParamsUnformatted).toHaveBeenCalledWith( + { + orgId: apiEouRes.ou.org_id, + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: undefined, + projectIds: null, + searchNameText: 'nonexistent', + offset: 0, + limit: 20, + }, + [] + ); + done(); + }); + + fixture.detectChanges(); + }); }); it('clearValue(): should clear all values', () => { @@ -275,12 +342,14 @@ describe('FyProjectSelectModalComponent', () => { }); }); - it('onDoneClick(): should dimiss the modal on clicking the done CTA', () => { + it('onDoneClick(): should dismiss the modal on clicking the done CTA', fakeAsync(() => { modalController.dismiss.and.resolveTo(true); component.onDoneClick(); + tick(); expect(modalController.dismiss).toHaveBeenCalledTimes(1); - }); + discardPeriodicTasks(); + })); describe('onElementSelect():', () => { it('should dismiss the modal with selected option', () => { @@ -308,35 +377,50 @@ describe('FyProjectSelectModalComponent', () => { }); }); - it('ngAfterViewInit(): show filtered projects and recently used items', fakeAsync((done) => { - spyOn(component, 'getRecentlyUsedItems').and.returnValue(of(expectedProjects)); - spyOn(component, 'getProjects').and.returnValue(of(labelledProjects)); + describe('ngAfterViewInit():', () => { + it('should get all categories by id if categoryIds are present', (done) => { + component.categoryIds = categoryIds; + component.ngAfterViewInit(); - utilityService.searchArrayStream.and.returnValue(() => of([{ label: 'project1', value: testProjectV2 }])); - fixture.detectChanges(); + component.activeCategories$.subscribe((categories) => { + expect(categoriesService.getCategoryById).toHaveBeenCalledTimes(categoryIds.length); + expect(categories.length).toBe(categoryIds.length); + expect(categories).toEqual([orgCategoryPaginated1[0]]); + done(); + }); + }); - component.ngAfterViewInit(); - inputElement.value = 'projects'; - inputElement.dispatchEvent(new Event('keyup')); + it('should show filtered projects and recently used items', fakeAsync((done) => { + spyOn(component, 'getRecentlyUsedItems').and.returnValue(of(expectedProjects)); + spyOn(component, 'getProjects').and.returnValue(of(labelledProjects)); - tick(300); - component.recentrecentlyUsedItems$.subscribe((res) => { - expect(res).toEqual(expectedProjects4); - }); + utilityService.searchArrayStream.and.returnValue(() => of([{ label: 'project1', value: testProjectV2 }])); + fixture.detectChanges(); - tick(300); - component.filteredOptions$.subscribe((res) => { - expect(res).toEqual(expectedLabelledProjects); - }); + component.ngAfterViewInit(); + inputElement.value = 'projects'; + inputElement.dispatchEvent(new Event('keyup')); - expect(component.getProjects).toHaveBeenCalledWith('projects'); - expect(component.getRecentlyUsedItems).toHaveBeenCalled(); - expect(utilityService.searchArrayStream).toHaveBeenCalledWith('projects'); + tick(300); + component.recentrecentlyUsedItems$.subscribe((res) => { + expect(res).toEqual(expectedProjects4); + }); - discardPeriodicTasks(); - })); + tick(300); + component.filteredOptions$.subscribe((res) => { + expect(res).toEqual(expectedLabelledProjects); + }); + + expect(component.getProjects).toHaveBeenCalledWith('projects'); + expect(component.getRecentlyUsedItems).toHaveBeenCalled(); + expect(utilityService.searchArrayStream).toHaveBeenCalledWith('projects'); + + discardPeriodicTasks(); + })); + }); it('should show label on the screen', () => { + component.activeCategories$ = of([]); component.label = 'Projects'; fixture.detectChanges(); diff --git a/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.ts b/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.ts index d61fea630f..8282b0d790 100644 --- a/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.ts +++ b/src/app/shared/components/fy-select-project/fy-select-modal/fy-select-project-modal.component.ts @@ -1,7 +1,7 @@ import { Component, AfterViewInit, ViewChild, ElementRef, Input, ChangeDetectorRef, TemplateRef } from '@angular/core'; -import { Observable, fromEvent, iif, of, from } from 'rxjs'; +import { Observable, fromEvent, iif, of, from, forkJoin } from 'rxjs'; import { ModalController } from '@ionic/angular'; -import { map, startWith, distinctUntilChanged, switchMap, concatMap, finalize, debounceTime } from 'rxjs/operators'; +import { map, startWith, distinctUntilChanged, switchMap, finalize, debounceTime, shareReplay } from 'rxjs/operators'; import { isEqual } from 'lodash'; import { ProjectsService } from 'src/app/core/services/projects.service'; import { AuthService } from 'src/app/core/services/auth.service'; @@ -12,6 +12,8 @@ import { OrgSettingsService } from 'src/app/core/services/org-settings.service'; import { OrgUserSettingsService } from 'src/app/core/services/org-user-settings.service'; import { OrgUserSettings } from 'src/app/core/models/org_user_settings.model'; import { ProjectOption } from 'src/app/core/models/project-options.model'; +import { OrgCategory } from 'src/app/core/models/v1/org-category.model'; +import { CategoriesService } from 'src/app/core/services/categories.service'; @Component({ selector: 'app-fy-select-modal', @@ -43,6 +45,8 @@ export class FyProjectSelectModalComponent implements AfterViewInit { isLoading = false; + activeCategories$: Observable; + constructor( private modalController: ModalController, private cdr: ChangeDetectorRef, @@ -51,7 +55,8 @@ export class FyProjectSelectModalComponent implements AfterViewInit { private recentLocalStorageItemsService: RecentLocalStorageItemsService, private utilityService: UtilityService, private orgUserSettingsService: OrgUserSettingsService, - private orgSettingsService: OrgSettingsService + private orgSettingsService: OrgSettingsService, + private categoriesService: CategoriesService ) {} getProjects(searchNameText: string): Observable { @@ -60,41 +65,49 @@ export class FyProjectSelectModalComponent implements AfterViewInit { // run ChangeDetectionRef.detectChanges to avoid 'expression has changed after it was checked error'. // More details about CDR: https://angular.io/api/core/ChangeDetectorRef this.cdr.detectChanges(); - const defaultProject$ = this.orgUserSettingsService.get().pipe( - switchMap((orgUserSettings) => { - if (orgUserSettings && orgUserSettings.preferences && orgUserSettings.preferences.default_project_id) { - return this.projectsService.getbyId(orgUserSettings.preferences.default_project_id); + const defaultProject$ = forkJoin({ + orgUserSettings: this.orgUserSettingsService.get(), + activeCategories: this.activeCategories$, + }).pipe( + switchMap(({ orgUserSettings, activeCategories }) => { + const defaultProjectId = orgUserSettings?.preferences?.default_project_id; + if (defaultProjectId) { + return this.projectsService.getbyId(defaultProjectId, activeCategories); } else { return of(null); } }) ); - return this.orgSettingsService.get().pipe( - switchMap((orgSettings) => - iif( + switchMap((orgSettings) => { + const allowedProjectIds$ = iif( () => orgSettings.advanced_projects.enable_individual_projects, this.orgUserSettingsService .get() .pipe(map((orgUserSettings: OrgUserSettings) => orgUserSettings.project_ids || [])), of(null) - ) - ), - concatMap((allowedProjectIds) => - from(this.authService.getEou()).pipe( - switchMap((eou) => - this.projectsService.getByParamsUnformatted({ - orgId: eou.ou.org_id, - active: true, - sortDirection: 'asc', - sortOrder: 'project_name', - orgCategoryIds: this.categoryIds, - projectIds: allowedProjectIds, - searchNameText, - offset: 0, - limit: 20, - }) - ) + ); + + return forkJoin({ + allowedProjectIds: allowedProjectIds$, + eou: from(this.authService.getEou()), + activeCategories: this.activeCategories$, + }); + }), + switchMap(({ allowedProjectIds, eou, activeCategories }) => + this.projectsService.getByParamsUnformatted( + { + orgId: eou.ou.org_id, + isEnabled: true, + sortDirection: 'asc', + sortOrder: 'name', + orgCategoryIds: this.categoryIds, + projectIds: allowedProjectIds, + searchNameText, + offset: 0, + limit: 20, + }, + activeCategories ) ), switchMap((projects) => { @@ -104,7 +117,6 @@ export class FyProjectSelectModalComponent implements AfterViewInit { if (defaultProject && !projects.some((project) => project.project_id === defaultProject.project_id)) { projects.push(defaultProject); } - return projects; }) ); @@ -129,10 +141,7 @@ export class FyProjectSelectModalComponent implements AfterViewInit { .concat(projects.map((project) => ({ label: project.project_name, value: project }))); }), finalize(() => { - // set isLoading to false this.isLoading = false; - // run ChangeDetectionRef.detectChanges to avoid 'expression has changed after it was checked error'. - // More details about CDR: https://angular.io/api/core/ChangeDetectorRef this.cdr.detectChanges(); }) ); @@ -160,7 +169,22 @@ export class FyProjectSelectModalComponent implements AfterViewInit { } } + getActiveCategories(): Observable { + const allCategories$ = this.categoriesService.getAll(); + + return allCategories$.pipe(map((catogories) => this.categoriesService.filterRequired(catogories))); + } + ngAfterViewInit(): void { + if (this.categoryIds?.length > 0) { + this.activeCategories$ = forkJoin( + this.categoryIds.map((id) => this.categoriesService.getCategoryById(parseInt(id, 10))) + ).pipe(shareReplay(1)); + } else { + // Fallback if this.categoryIds is empty + this.activeCategories$ = this.getActiveCategories().pipe(shareReplay(1)); + } + this.filteredOptions$ = fromEvent<{ target: HTMLInputElement }>(this.searchBarRef.nativeElement, 'keyup').pipe( map((event) => event.target.value), startWith(''),